mirror of
				https://github.com/pure-admin/vue-pure-admin.git
				synced 2025-11-03 13:44:47 +08:00 
			
		
		
		
	Refactor/tags (#332)
* refactor: tags * chore: update * chore: update * chore: update * chore: update
This commit is contained in:
		
							parent
							
								
									7c84d9eb70
								
							
						
					
					
						commit
						cbe539c727
					
				@ -123,13 +123,19 @@ const tabsRouter = {
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      path: "/tabs/detail",
 | 
			
		||||
      name: "TabDetail",
 | 
			
		||||
      path: "/tabs/query-detail",
 | 
			
		||||
      name: "TabQueryDetail",
 | 
			
		||||
      meta: {
 | 
			
		||||
        title: "",
 | 
			
		||||
        showLink: false,
 | 
			
		||||
        dynamicLevel: 3,
 | 
			
		||||
        refreshRedirect: "/tabs/index"
 | 
			
		||||
        // 不在menu菜单中显示
 | 
			
		||||
        showLink: false
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      path: "/tabs/params-detail/:id",
 | 
			
		||||
      component: "params-detail",
 | 
			
		||||
      name: "TabParamsDetail",
 | 
			
		||||
      meta: {
 | 
			
		||||
        showLink: false
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
 | 
			
		||||
@ -156,7 +156,7 @@ watch(
 | 
			
		||||
            >
 | 
			
		||||
              <el-divider class="tab-divider" border-style="dashed" />
 | 
			
		||||
              <el-scrollbar height="220px">
 | 
			
		||||
                <ul class="flex flex-wrap px-2 ml-2">
 | 
			
		||||
                <ul class="flex-wrap px-2 ml-2">
 | 
			
		||||
                  <li
 | 
			
		||||
                    v-for="(item, key) in pageList"
 | 
			
		||||
                    :key="key"
 | 
			
		||||
 | 
			
		||||
@ -428,7 +428,7 @@ function scrollInitMove() {
 | 
			
		||||
      if (timer) clearTimeout(timer);
 | 
			
		||||
      copyHtml.value = unref(slotList).innerHTML;
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        realBoxHeight.value = unref(realBox).offsetHeight;
 | 
			
		||||
        realBoxHeight.value = unref(realBox)?.offsetHeight;
 | 
			
		||||
        scrollMove();
 | 
			
		||||
      }, 0);
 | 
			
		||||
    } else {
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { ref, watch } from "vue";
 | 
			
		||||
import { isEqual } from "lodash-unified";
 | 
			
		||||
import { transformI18n } from "/@/plugins/i18n";
 | 
			
		||||
import { ref, watch, onMounted, toRaw } from "vue";
 | 
			
		||||
import { getParentPaths, findRouteByPath } from "/@/router/utils";
 | 
			
		||||
import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
 | 
			
		||||
import { useRoute, useRouter, RouteLocationMatched } from "vue-router";
 | 
			
		||||
@ -14,19 +14,24 @@ const multiTags: any = useMultiTagsStoreHook().multiTags;
 | 
			
		||||
 | 
			
		||||
const isDashboard = (route: RouteLocationMatched): boolean | string => {
 | 
			
		||||
  const name = route && (route.name as string);
 | 
			
		||||
  if (!name) {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  if (!name) return false;
 | 
			
		||||
  return name.trim().toLocaleLowerCase() === "Welcome".toLocaleLowerCase();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getBreadcrumb = (): void => {
 | 
			
		||||
  // 当前路由信息
 | 
			
		||||
  let currentRoute;
 | 
			
		||||
 | 
			
		||||
  if (Object.keys(route.query).length > 0) {
 | 
			
		||||
    multiTags.forEach(item => {
 | 
			
		||||
      if (isEqual(route.query, item?.query)) {
 | 
			
		||||
        currentRoute = item;
 | 
			
		||||
        currentRoute = toRaw(item);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  } else if (Object.keys(route.params).length > 0) {
 | 
			
		||||
    multiTags.forEach(item => {
 | 
			
		||||
      if (isEqual(route.params, item?.params)) {
 | 
			
		||||
        currentRoute = toRaw(item);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  } else {
 | 
			
		||||
@ -38,29 +43,12 @@ const getBreadcrumb = (): void => {
 | 
			
		||||
  let matched = [];
 | 
			
		||||
  // 获取每个父级路径对应的路由信息
 | 
			
		||||
  parentRoutes.forEach(path => {
 | 
			
		||||
    if (path !== "/") {
 | 
			
		||||
      matched.push(findRouteByPath(path, routes));
 | 
			
		||||
    }
 | 
			
		||||
    if (path !== "/") matched.push(findRouteByPath(path, routes));
 | 
			
		||||
  });
 | 
			
		||||
  if (router.currentRoute.value.meta?.refreshRedirect) {
 | 
			
		||||
    matched.unshift(
 | 
			
		||||
      findRouteByPath(
 | 
			
		||||
        router.currentRoute.value.meta.refreshRedirect as string,
 | 
			
		||||
        routes
 | 
			
		||||
      )
 | 
			
		||||
    );
 | 
			
		||||
  } else {
 | 
			
		||||
    // 过滤与子级相同标题的父级路由
 | 
			
		||||
    matched = matched.filter(item => {
 | 
			
		||||
      return !item.redirect || (item.redirect && item.children.length !== 1);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  if (currentRoute?.path !== "/welcome") {
 | 
			
		||||
    matched.push(currentRoute);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const first = matched[0];
 | 
			
		||||
  if (!isDashboard(first)) {
 | 
			
		||||
  if (currentRoute?.path !== "/welcome") matched.push(currentRoute);
 | 
			
		||||
 | 
			
		||||
  if (!isDashboard(matched[0])) {
 | 
			
		||||
    matched = [
 | 
			
		||||
      {
 | 
			
		||||
        path: "/welcome",
 | 
			
		||||
@ -70,60 +58,51 @@ const getBreadcrumb = (): void => {
 | 
			
		||||
    ].concat(matched);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  matched.forEach((item, index) => {
 | 
			
		||||
    if (currentRoute.query || currentRoute.params) return;
 | 
			
		||||
    if (item?.children) {
 | 
			
		||||
      item.children.forEach(v => {
 | 
			
		||||
        if (v.meta.title === item.meta.title) {
 | 
			
		||||
          matched.splice(index, 1);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  levelList.value = matched.filter(
 | 
			
		||||
    item => item?.meta && item?.meta.title !== false
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
getBreadcrumb();
 | 
			
		||||
const handleLink = (item: RouteLocationMatched): void => {
 | 
			
		||||
  const { redirect, path } = item;
 | 
			
		||||
  if (redirect) {
 | 
			
		||||
    router.push(redirect as any);
 | 
			
		||||
  } else {
 | 
			
		||||
    router.push(path);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  getBreadcrumb();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
watch(
 | 
			
		||||
  () => route.path,
 | 
			
		||||
  () => getBreadcrumb()
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
watch(
 | 
			
		||||
  () => route.query,
 | 
			
		||||
  () => getBreadcrumb()
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const handleLink = (item: RouteLocationMatched): any => {
 | 
			
		||||
  const { redirect, path } = item;
 | 
			
		||||
  if (redirect) {
 | 
			
		||||
    router.push(redirect.toString());
 | 
			
		||||
    return;
 | 
			
		||||
  () => {
 | 
			
		||||
    getBreadcrumb();
 | 
			
		||||
  }
 | 
			
		||||
  router.push(path);
 | 
			
		||||
};
 | 
			
		||||
);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <el-breadcrumb class="app-breadcrumb select-none" separator="/">
 | 
			
		||||
  <el-breadcrumb class="!leading-[50px] select-none" separator="/">
 | 
			
		||||
    <transition-group appear name="breadcrumb">
 | 
			
		||||
      <el-breadcrumb-item v-for="(item, index) in levelList" :key="item.path">
 | 
			
		||||
        <span
 | 
			
		||||
          v-if="item.redirect === 'noRedirect' || index == levelList.length - 1"
 | 
			
		||||
          class="no-redirect"
 | 
			
		||||
        >
 | 
			
		||||
          {{ transformI18n(item.meta.title) }}
 | 
			
		||||
        </span>
 | 
			
		||||
        <a v-else @click.prevent="handleLink(item)">
 | 
			
		||||
      <el-breadcrumb-item v-for="item in levelList" :key="item.path">
 | 
			
		||||
        <a @click.prevent="handleLink(item)">
 | 
			
		||||
          {{ transformI18n(item.meta.title) }}
 | 
			
		||||
        </a>
 | 
			
		||||
      </el-breadcrumb-item>
 | 
			
		||||
    </transition-group>
 | 
			
		||||
  </el-breadcrumb>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.app-breadcrumb.el-breadcrumb {
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
  line-height: 50px;
 | 
			
		||||
 | 
			
		||||
  .no-redirect {
 | 
			
		||||
    color: #97a8be;
 | 
			
		||||
    cursor: text;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 | 
			
		||||
@ -1,123 +1,53 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import {
 | 
			
		||||
  ref,
 | 
			
		||||
  watch,
 | 
			
		||||
  unref,
 | 
			
		||||
  toRaw,
 | 
			
		||||
  reactive,
 | 
			
		||||
  nextTick,
 | 
			
		||||
  computed,
 | 
			
		||||
  ComputedRef,
 | 
			
		||||
  CSSProperties,
 | 
			
		||||
  onBeforeMount,
 | 
			
		||||
  getCurrentInstance
 | 
			
		||||
} from "vue";
 | 
			
		||||
 | 
			
		||||
import close from "/@/assets/svg/close.svg?component";
 | 
			
		||||
import refresh from "/@/assets/svg/refresh.svg?component";
 | 
			
		||||
import closeAll from "/@/assets/svg/close_all.svg?component";
 | 
			
		||||
import closeLeft from "/@/assets/svg/close_left.svg?component";
 | 
			
		||||
import closeOther from "/@/assets/svg/close_other.svg?component";
 | 
			
		||||
import closeRight from "/@/assets/svg/close_right.svg?component";
 | 
			
		||||
 | 
			
		||||
import { useI18n } from "vue-i18n";
 | 
			
		||||
import { emitter } from "/@/utils/mitt";
 | 
			
		||||
import type { StorageConfigs } from "/#/index";
 | 
			
		||||
import { RouteConfigs } from "../../types";
 | 
			
		||||
import { useTags } from "../../hooks/useTag";
 | 
			
		||||
import { routerArrays } from "/@/layout/types";
 | 
			
		||||
import { useRoute, useRouter } from "vue-router";
 | 
			
		||||
import { isEqual, isEmpty } from "lodash-unified";
 | 
			
		||||
import { transformI18n, $t } from "/@/plugins/i18n";
 | 
			
		||||
import { RouteConfigs, tagsViewsType } from "../../types";
 | 
			
		||||
import { toggleClass, removeClass } from "@pureadmin/utils";
 | 
			
		||||
import { useResizeObserver, useDebounceFn } from "@vueuse/core";
 | 
			
		||||
import { useSettingStoreHook } from "/@/store/modules/settings";
 | 
			
		||||
import { handleAliveRoute, delAliveRoutes } from "/@/router/utils";
 | 
			
		||||
import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
 | 
			
		||||
import { usePermissionStoreHook } from "/@/store/modules/permission";
 | 
			
		||||
import { templateRef, useResizeObserver, useDebounceFn } from "@vueuse/core";
 | 
			
		||||
import {
 | 
			
		||||
  toggleClass,
 | 
			
		||||
  removeClass,
 | 
			
		||||
  hasClass,
 | 
			
		||||
  storageLocal
 | 
			
		||||
} from "@pureadmin/utils";
 | 
			
		||||
import { ref, watch, unref, toRaw, nextTick, onBeforeMount } from "vue";
 | 
			
		||||
 | 
			
		||||
const { t } = useI18n();
 | 
			
		||||
const route = useRoute();
 | 
			
		||||
const router = useRouter();
 | 
			
		||||
const translateX = ref<number>(0);
 | 
			
		||||
const activeIndex = ref<number>(-1);
 | 
			
		||||
let refreshButton = "refresh-button";
 | 
			
		||||
const instance = getCurrentInstance();
 | 
			
		||||
const pureSetting = useSettingStoreHook();
 | 
			
		||||
const tabDom = templateRef<HTMLElement | null>("tabDom", null);
 | 
			
		||||
const containerDom = templateRef<HTMLElement | null>("containerDom", null);
 | 
			
		||||
const scrollbarDom = templateRef<HTMLElement | null>("scrollbarDom", null);
 | 
			
		||||
const showTags =
 | 
			
		||||
  ref(storageLocal.getItem<StorageConfigs>("responsive-configure").hideTabs) ??
 | 
			
		||||
  "false";
 | 
			
		||||
// @ts-expect-error
 | 
			
		||||
let multiTags: ComputedRef<Array<RouteConfigs>> = computed(() => {
 | 
			
		||||
  return useMultiTagsStoreHook()?.multiTags;
 | 
			
		||||
});
 | 
			
		||||
const {
 | 
			
		||||
  route,
 | 
			
		||||
  router,
 | 
			
		||||
  visible,
 | 
			
		||||
  showTags,
 | 
			
		||||
  instance,
 | 
			
		||||
  multiTags,
 | 
			
		||||
  tagsViews,
 | 
			
		||||
  buttonTop,
 | 
			
		||||
  buttonLeft,
 | 
			
		||||
  showModel,
 | 
			
		||||
  translateX,
 | 
			
		||||
  activeIndex,
 | 
			
		||||
  getTabStyle,
 | 
			
		||||
  iconIsActive,
 | 
			
		||||
  linkIsActive,
 | 
			
		||||
  currentSelect,
 | 
			
		||||
  scheduleIsActive,
 | 
			
		||||
  getContextMenuStyle,
 | 
			
		||||
  closeMenu,
 | 
			
		||||
  onMounted,
 | 
			
		||||
  onMouseenter,
 | 
			
		||||
  onMouseleave,
 | 
			
		||||
  transformI18n
 | 
			
		||||
} = useTags();
 | 
			
		||||
 | 
			
		||||
const linkIsActive = computed(() => {
 | 
			
		||||
  return item => {
 | 
			
		||||
    if (Object.keys(route.query).length === 0) {
 | 
			
		||||
      if (route.path === item.path) {
 | 
			
		||||
        return "is-active";
 | 
			
		||||
      } else {
 | 
			
		||||
        return "";
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      if (isEqual(route?.query, item?.query)) {
 | 
			
		||||
        return "is-active";
 | 
			
		||||
      } else {
 | 
			
		||||
        return "";
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const scheduleIsActive = computed(() => {
 | 
			
		||||
  return item => {
 | 
			
		||||
    if (Object.keys(route.query).length === 0) {
 | 
			
		||||
      if (route.path === item.path) {
 | 
			
		||||
        return "schedule-active";
 | 
			
		||||
      } else {
 | 
			
		||||
        return "";
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      if (isEqual(route?.query, item?.query)) {
 | 
			
		||||
        return "schedule-active";
 | 
			
		||||
      } else {
 | 
			
		||||
        return "";
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const iconIsActive = computed(() => {
 | 
			
		||||
  return (item, index) => {
 | 
			
		||||
    if (index === 0) return;
 | 
			
		||||
    if (Object.keys(route.query).length === 0) {
 | 
			
		||||
      if (route.path === item.path) {
 | 
			
		||||
        return true;
 | 
			
		||||
      } else {
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      if (isEqual(route?.query, item?.query)) {
 | 
			
		||||
        return true;
 | 
			
		||||
      } else {
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
});
 | 
			
		||||
const tabDom = ref();
 | 
			
		||||
const containerDom = ref();
 | 
			
		||||
const scrollbarDom = ref();
 | 
			
		||||
 | 
			
		||||
const dynamicTagView = () => {
 | 
			
		||||
  const index = multiTags.value.findIndex(item => {
 | 
			
		||||
    if (item?.query) {
 | 
			
		||||
      return isEqual(route?.query, item?.query);
 | 
			
		||||
    if (item.query) {
 | 
			
		||||
      return isEqual(route.query, item.query);
 | 
			
		||||
    } else if (item.params) {
 | 
			
		||||
      return isEqual(route.params, item.params);
 | 
			
		||||
    } else {
 | 
			
		||||
      return item.path === route.path;
 | 
			
		||||
    }
 | 
			
		||||
@ -125,23 +55,9 @@ const dynamicTagView = () => {
 | 
			
		||||
  moveToView(index);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
watch([route], () => {
 | 
			
		||||
  activeIndex.value = -1;
 | 
			
		||||
  dynamicTagView();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
useResizeObserver(
 | 
			
		||||
  scrollbarDom,
 | 
			
		||||
  useDebounceFn(() => {
 | 
			
		||||
    dynamicTagView();
 | 
			
		||||
  }, 200)
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const tabNavPadding = 10;
 | 
			
		||||
const moveToView = (index: number): void => {
 | 
			
		||||
  if (!instance.refs["dynamic" + index]) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  const tabNavPadding = 10;
 | 
			
		||||
  if (!instance.refs["dynamic" + index]) return;
 | 
			
		||||
  const tabItemEl = instance.refs["dynamic" + index][0];
 | 
			
		||||
  const tabItemElOffsetLeft = (tabItemEl as HTMLElement)?.offsetLeft;
 | 
			
		||||
  const tabItemOffsetWidth = (tabItemEl as HTMLElement)?.offsetWidth;
 | 
			
		||||
@ -151,7 +67,6 @@ const moveToView = (index: number): void => {
 | 
			
		||||
    : 0;
 | 
			
		||||
  // 已有标签页总长度(包含溢出部分)
 | 
			
		||||
  const tabDomWidth = tabDom.value ? tabDom.value?.offsetWidth : 0;
 | 
			
		||||
 | 
			
		||||
  if (tabDomWidth < scrollbarDomWidth || tabItemElOffsetLeft === 0) {
 | 
			
		||||
    translateX.value = 0;
 | 
			
		||||
  } else if (tabItemElOffsetLeft < -translateX.value) {
 | 
			
		||||
@ -200,71 +115,6 @@ const handleScroll = (offset: number): void => {
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const tagsViews = reactive<Array<tagsViewsType>>([
 | 
			
		||||
  {
 | 
			
		||||
    icon: refresh,
 | 
			
		||||
    text: $t("buttons.hsreload"),
 | 
			
		||||
    divided: false,
 | 
			
		||||
    disabled: false,
 | 
			
		||||
    show: true
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    icon: close,
 | 
			
		||||
    text: $t("buttons.hscloseCurrentTab"),
 | 
			
		||||
    divided: false,
 | 
			
		||||
    disabled: multiTags.value.length > 1 ? false : true,
 | 
			
		||||
    show: true
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    icon: closeLeft,
 | 
			
		||||
    text: $t("buttons.hscloseLeftTabs"),
 | 
			
		||||
    divided: true,
 | 
			
		||||
    disabled: multiTags.value.length > 1 ? false : true,
 | 
			
		||||
    show: true
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    icon: closeRight,
 | 
			
		||||
    text: $t("buttons.hscloseRightTabs"),
 | 
			
		||||
    divided: false,
 | 
			
		||||
    disabled: multiTags.value.length > 1 ? false : true,
 | 
			
		||||
    show: true
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    icon: closeOther,
 | 
			
		||||
    text: $t("buttons.hscloseOtherTabs"),
 | 
			
		||||
    divided: true,
 | 
			
		||||
    disabled: multiTags.value.length > 2 ? false : true,
 | 
			
		||||
    show: true
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    icon: closeAll,
 | 
			
		||||
    text: $t("buttons.hscloseAllTabs"),
 | 
			
		||||
    divided: false,
 | 
			
		||||
    disabled: multiTags.value.length > 1 ? false : true,
 | 
			
		||||
    show: true
 | 
			
		||||
  }
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
// 显示模式,默认灵动模式显示
 | 
			
		||||
const showModel = ref(
 | 
			
		||||
  storageLocal.getItem<StorageConfigs>("responsive-configure")?.showModel ||
 | 
			
		||||
    "smart"
 | 
			
		||||
);
 | 
			
		||||
if (!showModel.value) {
 | 
			
		||||
  const configure = storageLocal.getItem<StorageConfigs>(
 | 
			
		||||
    "responsive-configure"
 | 
			
		||||
  );
 | 
			
		||||
  configure.showModel = "card";
 | 
			
		||||
  storageLocal.setItem("responsive-configure", configure);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let visible = ref(false);
 | 
			
		||||
let buttonLeft = ref(0);
 | 
			
		||||
let buttonTop = ref(0);
 | 
			
		||||
 | 
			
		||||
// 当前右键选中的路由信息
 | 
			
		||||
let currentSelect = ref({});
 | 
			
		||||
 | 
			
		||||
function dynamicRouteTag(value: string, parentPath: string): void {
 | 
			
		||||
  const hasValue = multiTags.value.some(item => {
 | 
			
		||||
    return item.path === value;
 | 
			
		||||
@ -292,8 +142,9 @@ function dynamicRouteTag(value: string, parentPath: string): void {
 | 
			
		||||
  concatPath(router.options.routes as any, value, parentPath);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 重新加载
 | 
			
		||||
/** 刷新路由 */
 | 
			
		||||
function onFresh() {
 | 
			
		||||
  const refreshButton = "refresh-button";
 | 
			
		||||
  toggleClass(true, refreshButton, document.querySelector(".rotate"));
 | 
			
		||||
  const { fullPath, query } = unref(route);
 | 
			
		||||
  router.replace({
 | 
			
		||||
@ -313,6 +164,10 @@ function deleteDynamicTag(obj: any, current: any, tag?: string) {
 | 
			
		||||
      if (item.path === obj.path) {
 | 
			
		||||
        return item.query === obj.query;
 | 
			
		||||
      }
 | 
			
		||||
    } else if (item.params) {
 | 
			
		||||
      if (item.path === obj.path) {
 | 
			
		||||
        return item.params === obj.params;
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      return item.path === obj.path;
 | 
			
		||||
    }
 | 
			
		||||
@ -351,24 +206,25 @@ function deleteDynamicTag(obj: any, current: any, tag?: string) {
 | 
			
		||||
      : handleAliveRoute(route.matched, "delete");
 | 
			
		||||
    // 如果删除当前激活tag就自动切换到最后一个tag
 | 
			
		||||
    if (tag === "left") return;
 | 
			
		||||
    nextTick(() => {
 | 
			
		||||
      router.push({
 | 
			
		||||
        path: newRoute[0].path,
 | 
			
		||||
        query: newRoute[0].query
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    if (newRoute[0]?.query) {
 | 
			
		||||
      router.push({ name: newRoute[0].name, query: newRoute[0].query });
 | 
			
		||||
    } else if (newRoute[0]?.params) {
 | 
			
		||||
      router.push({ name: newRoute[0].name, params: newRoute[0].params });
 | 
			
		||||
    } else {
 | 
			
		||||
      router.push({ path: newRoute[0].path });
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    // 删除缓存路由
 | 
			
		||||
    tag ? delAliveRoutes(delAliveRouteList) : delAliveRoutes([obj]);
 | 
			
		||||
    if (!multiTags.value.length) return;
 | 
			
		||||
    let isHasActiveTag = multiTags.value.some(item => {
 | 
			
		||||
      return item.path === route.path;
 | 
			
		||||
    });
 | 
			
		||||
    !isHasActiveTag &&
 | 
			
		||||
      router.push({
 | 
			
		||||
        path: newRoute[0].path,
 | 
			
		||||
        query: newRoute[0].query
 | 
			
		||||
      });
 | 
			
		||||
    if (multiTags.value.some(item => item.path === route.path)) return;
 | 
			
		||||
    if (newRoute[0]?.query) {
 | 
			
		||||
      router.push({ name: newRoute[0].name, query: newRoute[0].query });
 | 
			
		||||
    } else if (newRoute[0]?.params) {
 | 
			
		||||
      router.push({ name: newRoute[0].name, params: newRoute[0].params });
 | 
			
		||||
    } else {
 | 
			
		||||
      router.push({ path: newRoute[0].path });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -385,7 +241,8 @@ function onClickDrop(key, item, selectRoute?: RouteConfigs) {
 | 
			
		||||
      path: selectRoute.path,
 | 
			
		||||
      meta: selectRoute.meta,
 | 
			
		||||
      name: selectRoute.name,
 | 
			
		||||
      query: selectRoute.query
 | 
			
		||||
      query: selectRoute?.query,
 | 
			
		||||
      params: selectRoute?.params
 | 
			
		||||
    };
 | 
			
		||||
  } else {
 | 
			
		||||
    selectTagRoute = { path: route.path, meta: route.meta };
 | 
			
		||||
@ -394,7 +251,7 @@ function onClickDrop(key, item, selectRoute?: RouteConfigs) {
 | 
			
		||||
  // 当前路由信息
 | 
			
		||||
  switch (key) {
 | 
			
		||||
    case 0:
 | 
			
		||||
      // 重新加载
 | 
			
		||||
      // 刷新路由
 | 
			
		||||
      onFresh();
 | 
			
		||||
      break;
 | 
			
		||||
    case 1:
 | 
			
		||||
@ -433,15 +290,11 @@ function handleCommand(command: any) {
 | 
			
		||||
  onClickDrop(key, item);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 触发右键中菜单的点击事件
 | 
			
		||||
/** 触发右键中菜单的点击事件 */
 | 
			
		||||
function selectTag(key, item) {
 | 
			
		||||
  onClickDrop(key, item, currentSelect.value);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function closeMenu() {
 | 
			
		||||
  visible.value = false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function showMenus(value: boolean) {
 | 
			
		||||
  Array.of(1, 2, 3, 4, 5).forEach(v => {
 | 
			
		||||
    tagsViews[v].show = value;
 | 
			
		||||
@ -454,7 +307,7 @@ function disabledMenus(value: boolean) {
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 检查当前右键的菜单两边是否存在别的菜单,如果左侧的菜单是首页,则不显示关闭左侧标签页,如果右侧没有菜单,则不显示关闭右侧标签页
 | 
			
		||||
/** 检查当前右键的菜单两边是否存在别的菜单,如果左侧的菜单是首页,则不显示关闭左侧标签页,如果右侧没有菜单,则不显示关闭右侧标签页 */
 | 
			
		||||
function showMenuModel(
 | 
			
		||||
  currentPath: string,
 | 
			
		||||
  query: object = {},
 | 
			
		||||
@ -514,7 +367,7 @@ function openMenu(tag, e) {
 | 
			
		||||
    // 右键菜单为首页,只显示刷新
 | 
			
		||||
    showMenus(false);
 | 
			
		||||
    tagsViews[0].show = true;
 | 
			
		||||
  } else if (route.path !== tag.path) {
 | 
			
		||||
  } else if (route.path !== tag.path && route.name !== tag.name) {
 | 
			
		||||
    // 右键菜单不匹配当前路由,隐藏刷新
 | 
			
		||||
    tagsViews[0].show = false;
 | 
			
		||||
    showMenuModel(tag.path, tag.query);
 | 
			
		||||
@ -542,64 +395,37 @@ function openMenu(tag, e) {
 | 
			
		||||
  } else {
 | 
			
		||||
    buttonLeft.value = left;
 | 
			
		||||
  }
 | 
			
		||||
  pureSetting.hiddenSideBar
 | 
			
		||||
  useSettingStoreHook().hiddenSideBar
 | 
			
		||||
    ? (buttonTop.value = e.clientY)
 | 
			
		||||
    : (buttonTop.value = e.clientY - 40);
 | 
			
		||||
  setTimeout(() => {
 | 
			
		||||
  nextTick(() => {
 | 
			
		||||
    visible.value = true;
 | 
			
		||||
  }, 10);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 触发tags标签切换
 | 
			
		||||
function tagOnClick(item) {
 | 
			
		||||
  router.push({
 | 
			
		||||
    path: item?.path,
 | 
			
		||||
    query: item?.query
 | 
			
		||||
  });
 | 
			
		||||
  showMenuModel(item?.path, item?.query);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 鼠标移入
 | 
			
		||||
function onMouseenter(index) {
 | 
			
		||||
  if (index) activeIndex.value = index;
 | 
			
		||||
  if (unref(showModel) === "smart") {
 | 
			
		||||
    if (hasClass(instance.refs["schedule" + index][0], "schedule-active"))
 | 
			
		||||
      return;
 | 
			
		||||
    toggleClass(true, "schedule-in", instance.refs["schedule" + index][0]);
 | 
			
		||||
    toggleClass(false, "schedule-out", instance.refs["schedule" + index][0]);
 | 
			
		||||
/** 触发tags标签切换 */
 | 
			
		||||
function tagOnClick(item) {
 | 
			
		||||
  const { name, path } = item;
 | 
			
		||||
  if (name) {
 | 
			
		||||
    if (item.query) {
 | 
			
		||||
      router.push({
 | 
			
		||||
        name,
 | 
			
		||||
        query: item.query
 | 
			
		||||
      });
 | 
			
		||||
    } else if (item.params) {
 | 
			
		||||
      router.push({
 | 
			
		||||
        name,
 | 
			
		||||
        params: item.params
 | 
			
		||||
      });
 | 
			
		||||
    } else {
 | 
			
		||||
    if (hasClass(instance.refs["dynamic" + index][0], "card-active")) return;
 | 
			
		||||
    toggleClass(true, "card-in", instance.refs["dynamic" + index][0]);
 | 
			
		||||
    toggleClass(false, "card-out", instance.refs["dynamic" + index][0]);
 | 
			
		||||
      router.push({ name });
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    router.push({ path });
 | 
			
		||||
  }
 | 
			
		||||
  // showMenuModel(item?.path, item?.query);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 鼠标移出
 | 
			
		||||
function onMouseleave(index) {
 | 
			
		||||
  activeIndex.value = -1;
 | 
			
		||||
  if (unref(showModel) === "smart") {
 | 
			
		||||
    if (hasClass(instance.refs["schedule" + index][0], "schedule-active"))
 | 
			
		||||
      return;
 | 
			
		||||
    toggleClass(false, "schedule-in", instance.refs["schedule" + index][0]);
 | 
			
		||||
    toggleClass(true, "schedule-out", instance.refs["schedule" + index][0]);
 | 
			
		||||
  } else {
 | 
			
		||||
    if (hasClass(instance.refs["dynamic" + index][0], "card-active")) return;
 | 
			
		||||
    toggleClass(false, "card-in", instance.refs["dynamic" + index][0]);
 | 
			
		||||
    toggleClass(true, "card-out", instance.refs["dynamic" + index][0]);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
watch(
 | 
			
		||||
  () => visible.value,
 | 
			
		||||
  val => {
 | 
			
		||||
    if (val) {
 | 
			
		||||
      document.body.addEventListener("click", closeMenu);
 | 
			
		||||
    } else {
 | 
			
		||||
      document.body.removeEventListener("click", closeMenu);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
onBeforeMount(() => {
 | 
			
		||||
  if (!instance) return;
 | 
			
		||||
 | 
			
		||||
@ -626,14 +452,18 @@ onBeforeMount(() => {
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const getTabStyle = computed((): CSSProperties => {
 | 
			
		||||
  return {
 | 
			
		||||
    transform: `translateX(${translateX.value}px)`
 | 
			
		||||
  };
 | 
			
		||||
watch([route], () => {
 | 
			
		||||
  activeIndex.value = -1;
 | 
			
		||||
  dynamicTagView();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const getContextMenuStyle = computed((): CSSProperties => {
 | 
			
		||||
  return { left: buttonLeft.value + "px", top: buttonTop.value + "px" };
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  useResizeObserver(
 | 
			
		||||
    scrollbarDom,
 | 
			
		||||
    useDebounceFn(() => {
 | 
			
		||||
      dynamicTagView();
 | 
			
		||||
    }, 200)
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
@ -705,7 +535,7 @@ const getContextMenuStyle = computed((): CSSProperties => {
 | 
			
		||||
        >
 | 
			
		||||
          <li v-if="item.show" @click="selectTag(key, item)">
 | 
			
		||||
            <component :is="toRaw(item.icon)" :key="key" />
 | 
			
		||||
            {{ t(item.text) }}
 | 
			
		||||
            {{ transformI18n(item.text) }}
 | 
			
		||||
          </li>
 | 
			
		||||
        </div>
 | 
			
		||||
      </ul>
 | 
			
		||||
@ -714,7 +544,7 @@ const getContextMenuStyle = computed((): CSSProperties => {
 | 
			
		||||
    <ul class="right-button">
 | 
			
		||||
      <li>
 | 
			
		||||
        <span
 | 
			
		||||
          :title="t('buttons.hsrefreshRoute')"
 | 
			
		||||
          :title="transformI18n('buttons.hsrefreshRoute')"
 | 
			
		||||
          class="el-icon-refresh-right rotate"
 | 
			
		||||
          @click="onFresh"
 | 
			
		||||
        >
 | 
			
		||||
@ -742,7 +572,7 @@ const getContextMenuStyle = computed((): CSSProperties => {
 | 
			
		||||
                  :key="key"
 | 
			
		||||
                  style="margin-right: 6px"
 | 
			
		||||
                />
 | 
			
		||||
                {{ t(item.text) }}
 | 
			
		||||
                {{ transformI18n(item.text) }}
 | 
			
		||||
              </el-dropdown-item>
 | 
			
		||||
            </el-dropdown-menu>
 | 
			
		||||
          </template>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										218
									
								
								src/layout/hooks/useTag.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										218
									
								
								src/layout/hooks/useTag.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,218 @@
 | 
			
		||||
import {
 | 
			
		||||
  ref,
 | 
			
		||||
  unref,
 | 
			
		||||
  watch,
 | 
			
		||||
  computed,
 | 
			
		||||
  reactive,
 | 
			
		||||
  onMounted,
 | 
			
		||||
  CSSProperties,
 | 
			
		||||
  getCurrentInstance
 | 
			
		||||
} from "vue";
 | 
			
		||||
import { tagsViewsType } from "../types";
 | 
			
		||||
import { isEqual } from "lodash-unified";
 | 
			
		||||
import type { StorageConfigs } from "/#/index";
 | 
			
		||||
import { useEventListener } from "@vueuse/core";
 | 
			
		||||
import { useRoute, useRouter } from "vue-router";
 | 
			
		||||
import { transformI18n, $t } from "/@/plugins/i18n";
 | 
			
		||||
import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
 | 
			
		||||
import { storageLocal, toggleClass, hasClass } from "@pureadmin/utils";
 | 
			
		||||
 | 
			
		||||
import close from "/@/assets/svg/close.svg?component";
 | 
			
		||||
import refresh from "/@/assets/svg/refresh.svg?component";
 | 
			
		||||
import closeAll from "/@/assets/svg/close_all.svg?component";
 | 
			
		||||
import closeLeft from "/@/assets/svg/close_left.svg?component";
 | 
			
		||||
import closeOther from "/@/assets/svg/close_other.svg?component";
 | 
			
		||||
import closeRight from "/@/assets/svg/close_right.svg?component";
 | 
			
		||||
 | 
			
		||||
export function useTags() {
 | 
			
		||||
  const route = useRoute();
 | 
			
		||||
  const router = useRouter();
 | 
			
		||||
  const instance = getCurrentInstance();
 | 
			
		||||
 | 
			
		||||
  const buttonTop = ref(0);
 | 
			
		||||
  const buttonLeft = ref(0);
 | 
			
		||||
  const translateX = ref(0);
 | 
			
		||||
  const visible = ref(false);
 | 
			
		||||
  const activeIndex = ref(-1);
 | 
			
		||||
  // 当前右键选中的路由信息
 | 
			
		||||
  const currentSelect = ref({});
 | 
			
		||||
 | 
			
		||||
  /** 显示模式,默认灵动模式 */
 | 
			
		||||
  const showModel = ref(
 | 
			
		||||
    storageLocal.getItem<StorageConfigs>("responsive-configure")?.showModel ||
 | 
			
		||||
      "smart"
 | 
			
		||||
  );
 | 
			
		||||
  /** 是否隐藏标签页,默认显示 */
 | 
			
		||||
  const showTags =
 | 
			
		||||
    ref(
 | 
			
		||||
      storageLocal.getItem<StorageConfigs>("responsive-configure").hideTabs
 | 
			
		||||
    ) ?? ref("false");
 | 
			
		||||
  const multiTags: any = computed(() => {
 | 
			
		||||
    return useMultiTagsStoreHook().multiTags;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const tagsViews = reactive<Array<tagsViewsType>>([
 | 
			
		||||
    {
 | 
			
		||||
      icon: refresh,
 | 
			
		||||
      text: $t("buttons.hsreload"),
 | 
			
		||||
      divided: false,
 | 
			
		||||
      disabled: false,
 | 
			
		||||
      show: true
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      icon: close,
 | 
			
		||||
      text: $t("buttons.hscloseCurrentTab"),
 | 
			
		||||
      divided: false,
 | 
			
		||||
      disabled: multiTags.value.length > 1 ? false : true,
 | 
			
		||||
      show: true
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      icon: closeLeft,
 | 
			
		||||
      text: $t("buttons.hscloseLeftTabs"),
 | 
			
		||||
      divided: true,
 | 
			
		||||
      disabled: multiTags.value.length > 1 ? false : true,
 | 
			
		||||
      show: true
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      icon: closeRight,
 | 
			
		||||
      text: $t("buttons.hscloseRightTabs"),
 | 
			
		||||
      divided: false,
 | 
			
		||||
      disabled: multiTags.value.length > 1 ? false : true,
 | 
			
		||||
      show: true
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      icon: closeOther,
 | 
			
		||||
      text: $t("buttons.hscloseOtherTabs"),
 | 
			
		||||
      divided: true,
 | 
			
		||||
      disabled: multiTags.value.length > 2 ? false : true,
 | 
			
		||||
      show: true
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      icon: closeAll,
 | 
			
		||||
      text: $t("buttons.hscloseAllTabs"),
 | 
			
		||||
      divided: false,
 | 
			
		||||
      disabled: multiTags.value.length > 1 ? false : true,
 | 
			
		||||
      show: true
 | 
			
		||||
    }
 | 
			
		||||
  ]);
 | 
			
		||||
 | 
			
		||||
  function conditionHandle(item, previous, next) {
 | 
			
		||||
    if (
 | 
			
		||||
      Object.keys(route.query).length === 0 &&
 | 
			
		||||
      Object.keys(route.params).length === 0
 | 
			
		||||
    ) {
 | 
			
		||||
      return route.path === item.path ? previous : next;
 | 
			
		||||
    } else if (Object.keys(route.query).length > 0) {
 | 
			
		||||
      return isEqual(route.query, item.query) ? previous : next;
 | 
			
		||||
    } else {
 | 
			
		||||
      return isEqual(route.params, item.params) ? previous : next;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const iconIsActive = computed(() => {
 | 
			
		||||
    return (item, index) => {
 | 
			
		||||
      if (index === 0) return;
 | 
			
		||||
      return conditionHandle(item, true, false);
 | 
			
		||||
    };
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const linkIsActive = computed(() => {
 | 
			
		||||
    return item => {
 | 
			
		||||
      return conditionHandle(item, "is-active", "");
 | 
			
		||||
    };
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const scheduleIsActive = computed(() => {
 | 
			
		||||
    return item => {
 | 
			
		||||
      return conditionHandle(item, "schedule-active", "");
 | 
			
		||||
    };
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const getTabStyle = computed((): CSSProperties => {
 | 
			
		||||
    return {
 | 
			
		||||
      transform: `translateX(${translateX.value}px)`
 | 
			
		||||
    };
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const getContextMenuStyle = computed((): CSSProperties => {
 | 
			
		||||
    return { left: buttonLeft.value + "px", top: buttonTop.value + "px" };
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const closeMenu = () => {
 | 
			
		||||
    visible.value = false;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  /** 鼠标移入添加激活样式 */
 | 
			
		||||
  function onMouseenter(index) {
 | 
			
		||||
    if (index) activeIndex.value = index;
 | 
			
		||||
    if (unref(showModel) === "smart") {
 | 
			
		||||
      if (hasClass(instance.refs["schedule" + index][0], "schedule-active"))
 | 
			
		||||
        return;
 | 
			
		||||
      toggleClass(true, "schedule-in", instance.refs["schedule" + index][0]);
 | 
			
		||||
      toggleClass(false, "schedule-out", instance.refs["schedule" + index][0]);
 | 
			
		||||
    } else {
 | 
			
		||||
      if (hasClass(instance.refs["dynamic" + index][0], "card-active")) return;
 | 
			
		||||
      toggleClass(true, "card-in", instance.refs["dynamic" + index][0]);
 | 
			
		||||
      toggleClass(false, "card-out", instance.refs["dynamic" + index][0]);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** 鼠标移出恢复默认样式 */
 | 
			
		||||
  function onMouseleave(index) {
 | 
			
		||||
    activeIndex.value = -1;
 | 
			
		||||
    if (unref(showModel) === "smart") {
 | 
			
		||||
      if (hasClass(instance.refs["schedule" + index][0], "schedule-active"))
 | 
			
		||||
        return;
 | 
			
		||||
      toggleClass(false, "schedule-in", instance.refs["schedule" + index][0]);
 | 
			
		||||
      toggleClass(true, "schedule-out", instance.refs["schedule" + index][0]);
 | 
			
		||||
    } else {
 | 
			
		||||
      if (hasClass(instance.refs["dynamic" + index][0], "card-active")) return;
 | 
			
		||||
      toggleClass(false, "card-in", instance.refs["dynamic" + index][0]);
 | 
			
		||||
      toggleClass(true, "card-out", instance.refs["dynamic" + index][0]);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onMounted(() => {
 | 
			
		||||
    if (!showModel.value) {
 | 
			
		||||
      const configure = storageLocal.getItem<StorageConfigs>(
 | 
			
		||||
        "responsive-configure"
 | 
			
		||||
      );
 | 
			
		||||
      configure.showModel = "card";
 | 
			
		||||
      storageLocal.setItem("responsive-configure", configure);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  watch(
 | 
			
		||||
    () => visible.value,
 | 
			
		||||
    () => {
 | 
			
		||||
      useEventListener(document, "click", closeMenu);
 | 
			
		||||
    }
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    route,
 | 
			
		||||
    router,
 | 
			
		||||
    visible,
 | 
			
		||||
    showTags,
 | 
			
		||||
    instance,
 | 
			
		||||
    multiTags,
 | 
			
		||||
    showModel,
 | 
			
		||||
    tagsViews,
 | 
			
		||||
    buttonTop,
 | 
			
		||||
    buttonLeft,
 | 
			
		||||
    translateX,
 | 
			
		||||
    activeIndex,
 | 
			
		||||
    getTabStyle,
 | 
			
		||||
    iconIsActive,
 | 
			
		||||
    linkIsActive,
 | 
			
		||||
    currentSelect,
 | 
			
		||||
    scheduleIsActive,
 | 
			
		||||
    getContextMenuStyle,
 | 
			
		||||
    $t,
 | 
			
		||||
    closeMenu,
 | 
			
		||||
    onMounted,
 | 
			
		||||
    onMouseenter,
 | 
			
		||||
    onMouseleave,
 | 
			
		||||
    transformI18n
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
@ -22,6 +22,7 @@ export type RouteConfigs = {
 | 
			
		||||
  path?: string;
 | 
			
		||||
  parentPath?: string;
 | 
			
		||||
  query?: object;
 | 
			
		||||
  params?: object;
 | 
			
		||||
  meta?: routeMetaType;
 | 
			
		||||
  children?: RouteConfigs[];
 | 
			
		||||
  name?: string;
 | 
			
		||||
 | 
			
		||||
@ -8,17 +8,14 @@ import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
 | 
			
		||||
import { usePermissionStoreHook } from "/@/store/modules/permission";
 | 
			
		||||
import {
 | 
			
		||||
  Router,
 | 
			
		||||
  RouteMeta,
 | 
			
		||||
  createRouter,
 | 
			
		||||
  RouteRecordRaw,
 | 
			
		||||
  RouteComponent,
 | 
			
		||||
  RouteRecordName
 | 
			
		||||
  RouteComponent
 | 
			
		||||
} from "vue-router";
 | 
			
		||||
import {
 | 
			
		||||
  ascending,
 | 
			
		||||
  initRouter,
 | 
			
		||||
  getHistoryMode,
 | 
			
		||||
  getParentPaths,
 | 
			
		||||
  findRouteByPath,
 | 
			
		||||
  handleAliveRoute,
 | 
			
		||||
  formatTwoStageRoutes,
 | 
			
		||||
@ -148,32 +145,6 @@ router.beforeEach((to: toRouteType, _from, next) => {
 | 
			
		||||
      if (usePermissionStoreHook().wholeMenus.length === 0)
 | 
			
		||||
        initRouter(name.username).then((router: Router) => {
 | 
			
		||||
          if (!useMultiTagsStoreHook().getMultiTagsCache) {
 | 
			
		||||
            const handTag = (
 | 
			
		||||
              path: string,
 | 
			
		||||
              parentPath: string,
 | 
			
		||||
              name: RouteRecordName,
 | 
			
		||||
              meta: RouteMeta
 | 
			
		||||
            ): void => {
 | 
			
		||||
              useMultiTagsStoreHook().handleTags("push", {
 | 
			
		||||
                path,
 | 
			
		||||
                parentPath,
 | 
			
		||||
                name,
 | 
			
		||||
                meta
 | 
			
		||||
              });
 | 
			
		||||
            };
 | 
			
		||||
            // 未开启标签页缓存,刷新页面重定向到顶级路由(参考标签页操作例子,只针对静态路由)
 | 
			
		||||
            if (to.meta?.refreshRedirect) {
 | 
			
		||||
              const routes: any = router.options.routes;
 | 
			
		||||
              const { refreshRedirect } = to.meta;
 | 
			
		||||
              const { name, meta } = findRouteByPath(refreshRedirect, routes);
 | 
			
		||||
              handTag(
 | 
			
		||||
                refreshRedirect,
 | 
			
		||||
                getParentPaths(refreshRedirect, routes)[1],
 | 
			
		||||
                name,
 | 
			
		||||
                meta
 | 
			
		||||
              );
 | 
			
		||||
              return router.push(refreshRedirect);
 | 
			
		||||
            } else {
 | 
			
		||||
            const { path } = to;
 | 
			
		||||
            const index = findIndex(remainingRouter, v => {
 | 
			
		||||
              return v.path == path;
 | 
			
		||||
@ -183,34 +154,13 @@ router.beforeEach((to: toRouteType, _from, next) => {
 | 
			
		||||
                ? router.options.routes[0].children
 | 
			
		||||
                : router.options.routes;
 | 
			
		||||
            const route = findRouteByPath(path, routes);
 | 
			
		||||
              const routePartent = getParentPaths(path, routes);
 | 
			
		||||
              // 未开启标签页缓存,刷新页面重定向到顶级路由(参考标签页操作例子,只针对动态路由)
 | 
			
		||||
              if (
 | 
			
		||||
                path !== routes[0].path &&
 | 
			
		||||
                route?.meta?.rank !== 0 &&
 | 
			
		||||
                routePartent.length === 0
 | 
			
		||||
              ) {
 | 
			
		||||
                if (!route?.meta?.refreshRedirect) return;
 | 
			
		||||
                const { name, meta } = findRouteByPath(
 | 
			
		||||
                  route.meta.refreshRedirect,
 | 
			
		||||
                  routes
 | 
			
		||||
                );
 | 
			
		||||
                handTag(
 | 
			
		||||
                  route.meta?.refreshRedirect,
 | 
			
		||||
                  getParentPaths(route.meta?.refreshRedirect, routes)[0],
 | 
			
		||||
                  name,
 | 
			
		||||
                  meta
 | 
			
		||||
                );
 | 
			
		||||
                return router.push(route.meta?.refreshRedirect);
 | 
			
		||||
              } else {
 | 
			
		||||
                handTag(
 | 
			
		||||
                  route.path,
 | 
			
		||||
                  routePartent[routePartent.length - 1],
 | 
			
		||||
                  route.name,
 | 
			
		||||
                  route.meta
 | 
			
		||||
                );
 | 
			
		||||
                return router.push(path);
 | 
			
		||||
              }
 | 
			
		||||
            // query、params模式路由传参数的标签页不在此处处理
 | 
			
		||||
            if (route && route.meta?.title) {
 | 
			
		||||
              useMultiTagsStoreHook().handleTags("push", {
 | 
			
		||||
                path: route.path,
 | 
			
		||||
                name: route.name,
 | 
			
		||||
                meta: route.meta
 | 
			
		||||
              });
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          router.push(to.fullPath);
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,6 @@ import { RouteLocationNormalized } from "vue-router";
 | 
			
		||||
export interface toRouteType extends RouteLocationNormalized {
 | 
			
		||||
  meta: {
 | 
			
		||||
    keepAlive?: boolean;
 | 
			
		||||
    refreshRedirect: string;
 | 
			
		||||
    dynamicLevel?: string;
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -7,6 +7,7 @@ import {
 | 
			
		||||
  RouteRecordNormalized
 | 
			
		||||
} from "vue-router";
 | 
			
		||||
import { router } from "./index";
 | 
			
		||||
import { isProxy, toRaw } from "vue";
 | 
			
		||||
import { loadEnv } from "../../build";
 | 
			
		||||
import { cloneDeep } from "lodash-unified";
 | 
			
		||||
import { useTimeoutFn } from "@vueuse/core";
 | 
			
		||||
@ -86,7 +87,7 @@ function getParentPaths(path: string, routes: RouteRecordRaw[]) {
 | 
			
		||||
function findRouteByPath(path: string, routes: RouteRecordRaw[]) {
 | 
			
		||||
  let res = routes.find((item: { path: string }) => item.path == path);
 | 
			
		||||
  if (res) {
 | 
			
		||||
    return res;
 | 
			
		||||
    return isProxy(res) ? toRaw(res) : res;
 | 
			
		||||
  } else {
 | 
			
		||||
    for (let i = 0; i < routes.length; i++) {
 | 
			
		||||
      if (
 | 
			
		||||
@ -95,7 +96,7 @@ function findRouteByPath(path: string, routes: RouteRecordRaw[]) {
 | 
			
		||||
      ) {
 | 
			
		||||
        res = findRouteByPath(path, routes[i].children);
 | 
			
		||||
        if (res) {
 | 
			
		||||
          return res;
 | 
			
		||||
          return isProxy(res) ? toRaw(res) : res;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -48,9 +48,13 @@ export const useMultiTagsStore = defineStore({
 | 
			
		||||
        case "push":
 | 
			
		||||
          {
 | 
			
		||||
            const tagVal = value as multiType;
 | 
			
		||||
            // 不添加到标签页
 | 
			
		||||
            if (tagVal?.meta?.hiddenTag) return;
 | 
			
		||||
            // 如果是外链无需添加信息到标签页
 | 
			
		||||
            if (isUrl(tagVal?.name)) return;
 | 
			
		||||
            // 如果title为空拒绝添加空信息到标签页
 | 
			
		||||
            if (tagVal?.meta?.title.length === 0) return;
 | 
			
		||||
            const tagPath = tagVal?.path;
 | 
			
		||||
            const tagPath = tagVal.path;
 | 
			
		||||
            // 判断tag是否已存在
 | 
			
		||||
            const tagHasExits = this.multiTags.some(tag => {
 | 
			
		||||
              return tag.path === tagPath;
 | 
			
		||||
@ -58,20 +62,24 @@ export const useMultiTagsStore = defineStore({
 | 
			
		||||
 | 
			
		||||
            // 判断tag中的query键值是否相等
 | 
			
		||||
            const tagQueryHasExits = this.multiTags.some(tag => {
 | 
			
		||||
              return isEqual(tag.query, tagVal?.query);
 | 
			
		||||
              return isEqual(tag?.query, tagVal?.query);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (tagHasExits && tagQueryHasExits) return;
 | 
			
		||||
            // 判断tag中的params键值是否相等
 | 
			
		||||
            const tagParamsHasExits = this.multiTags.some(tag => {
 | 
			
		||||
              return isEqual(tag?.params, tagVal?.params);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (tagHasExits && tagQueryHasExits && tagParamsHasExits) return;
 | 
			
		||||
 | 
			
		||||
            // 动态路由可打开的最大数量
 | 
			
		||||
            const dynamicLevel = tagVal?.meta?.dynamicLevel ?? -1;
 | 
			
		||||
            if (dynamicLevel > 0) {
 | 
			
		||||
              // dynamicLevel动态路由可打开的数量
 | 
			
		||||
              // 获取到已经打开的动态路由数, 判断是否大于dynamicLevel
 | 
			
		||||
              if (
 | 
			
		||||
                this.multiTags.filter(e => e?.path === tagPath).length >=
 | 
			
		||||
                dynamicLevel
 | 
			
		||||
              ) {
 | 
			
		||||
                // 关闭第一个
 | 
			
		||||
                // 如果当前已打开的动态路由数大于dynamicLevel,替换第一个动态路由标签
 | 
			
		||||
                const index = this.multiTags.findIndex(
 | 
			
		||||
                  item => item?.path === tagPath
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
@ -27,6 +27,7 @@ export type multiType = {
 | 
			
		||||
  name: string;
 | 
			
		||||
  meta: any;
 | 
			
		||||
  query?: object;
 | 
			
		||||
  params?: object;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type setType = {
 | 
			
		||||
 | 
			
		||||
@ -5,26 +5,48 @@ import { onBeforeMount } from "vue";
 | 
			
		||||
export function useDetail() {
 | 
			
		||||
  const route = useRoute();
 | 
			
		||||
  const router = useRouter();
 | 
			
		||||
  const id = route.query?.id ?? -1;
 | 
			
		||||
  const id = route.query?.id ? route.query?.id : route.params?.id;
 | 
			
		||||
 | 
			
		||||
  function toDetail(index: number | string | string[] | number[]) {
 | 
			
		||||
  function toDetail(
 | 
			
		||||
    index: number | string | string[] | number[],
 | 
			
		||||
    model: string
 | 
			
		||||
  ) {
 | 
			
		||||
    if (model === "query") {
 | 
			
		||||
      // 保存信息到标签页
 | 
			
		||||
      useMultiTagsStoreHook().handleTags("push", {
 | 
			
		||||
      path: `/tabs/detail`,
 | 
			
		||||
      parentPath: route.matched[0].path,
 | 
			
		||||
      name: "TabDetail",
 | 
			
		||||
        path: `/tabs/query-detail`,
 | 
			
		||||
        name: "TabQueryDetail",
 | 
			
		||||
        query: { id: String(index) },
 | 
			
		||||
        meta: {
 | 
			
		||||
        title: { zh: `No.${index} - 详情信息`, en: `No.${index} - DetailInfo` },
 | 
			
		||||
        showLink: false,
 | 
			
		||||
          title: {
 | 
			
		||||
            zh: `No.${index} - 详情信息`,
 | 
			
		||||
            en: `No.${index} - DetailInfo`
 | 
			
		||||
          },
 | 
			
		||||
          // 最大打开标签数
 | 
			
		||||
          dynamicLevel: 3
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    router.push({ name: "TabDetail", query: { id: String(index) } });
 | 
			
		||||
      // 路由跳转
 | 
			
		||||
      router.push({ name: "TabQueryDetail", query: { id: String(index) } });
 | 
			
		||||
    } else {
 | 
			
		||||
      useMultiTagsStoreHook().handleTags("push", {
 | 
			
		||||
        path: `/tabs/params-detail/:id`,
 | 
			
		||||
        name: "TabParamsDetail",
 | 
			
		||||
        params: { id: String(index) },
 | 
			
		||||
        meta: {
 | 
			
		||||
          title: {
 | 
			
		||||
            zh: `No.${index} - 详情信息`,
 | 
			
		||||
            en: `No.${index} - DetailInfo`
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
      router.push({ name: "TabParamsDetail", params: { id: String(index) } });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function initToDetail() {
 | 
			
		||||
  function initToDetail(model) {
 | 
			
		||||
    onBeforeMount(() => {
 | 
			
		||||
      if (id) toDetail(id);
 | 
			
		||||
      if (id) toDetail(id, model);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -6,8 +6,8 @@ import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
 | 
			
		||||
import { usePermissionStoreHook } from "/@/store/modules/permission";
 | 
			
		||||
import {
 | 
			
		||||
  deleteChildren,
 | 
			
		||||
  appendFieldByUniqueId,
 | 
			
		||||
  getNodeByUniqueId
 | 
			
		||||
  getNodeByUniqueId,
 | 
			
		||||
  appendFieldByUniqueId
 | 
			
		||||
} from "@pureadmin/utils";
 | 
			
		||||
import { useDetail } from "./hooks";
 | 
			
		||||
 | 
			
		||||
@ -50,9 +50,32 @@ function onCloseTags() {
 | 
			
		||||
    <template #header>
 | 
			
		||||
      <div>标签页复用,超出限制自动关闭(使用场景: 动态路由)</div>
 | 
			
		||||
    </template>
 | 
			
		||||
    <el-button v-for="index in 6" :key="index" @click="toDetail(index)">
 | 
			
		||||
    <div class="flex-wrap items-center">
 | 
			
		||||
      <p>query传参模式:</p>
 | 
			
		||||
      <el-button
 | 
			
		||||
        class="m-2"
 | 
			
		||||
        v-for="index in 6"
 | 
			
		||||
        :key="index"
 | 
			
		||||
        @click="toDetail(index, 'query')"
 | 
			
		||||
      >
 | 
			
		||||
        打开{{ index }}详情页
 | 
			
		||||
      </el-button>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <el-divider />
 | 
			
		||||
 | 
			
		||||
    <div class="flex-wrap items-center">
 | 
			
		||||
      <p>params传参模式:</p>
 | 
			
		||||
      <el-button
 | 
			
		||||
        class="m-2"
 | 
			
		||||
        v-for="index in 6"
 | 
			
		||||
        :key="index"
 | 
			
		||||
        @click="toDetail(index, 'params')"
 | 
			
		||||
      >
 | 
			
		||||
        打开{{ index }}详情页
 | 
			
		||||
      </el-button>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <el-divider />
 | 
			
		||||
    <TreeSelect
 | 
			
		||||
      class="w-300px"
 | 
			
		||||
@ -80,19 +103,27 @@ function onCloseTags() {
 | 
			
		||||
        <span>{{ transformI18n(data.meta.title) }}</span>
 | 
			
		||||
      </template>
 | 
			
		||||
    </TreeSelect>
 | 
			
		||||
    <el-button class="ml-2" @click="onCloseTags">关闭标签</el-button>
 | 
			
		||||
    <br />
 | 
			
		||||
    <p class="mt-4">
 | 
			
		||||
      注意:此demo并未开启标签页缓存,如果需要在
 | 
			
		||||
      <span class="text-red-500">刷新页面</span>
 | 
			
		||||
      的时候同时
 | 
			
		||||
      <span class="text-red-500">保留标签页的显示</span>
 | 
			
		||||
      或者
 | 
			
		||||
      <span class="text-red-500">保留url的参数</span>
 | 
			
		||||
      ,那么就需要开启标签页持久化。
 | 
			
		||||
      <br />
 | 
			
		||||
      开启方式:在页面最右上角有个设置的小图标,点进去,会看到项目配置面板,找到标签页持久化开启即可。
 | 
			
		||||
    </p>
 | 
			
		||||
    <el-button class="m-2" @click="onCloseTags">关闭标签</el-button>
 | 
			
		||||
 | 
			
		||||
    <el-divider />
 | 
			
		||||
    <el-button @click="$router.push({ name: 'Menu1-2-2' })">
 | 
			
		||||
      跳转页内菜单(传name对象,优先推荐)
 | 
			
		||||
    </el-button>
 | 
			
		||||
    <el-button @click="$router.push('/nested/menu1/menu1-2/menu1-2-2')">
 | 
			
		||||
      跳转页内菜单(直接传要跳转的路径)
 | 
			
		||||
    </el-button>
 | 
			
		||||
    <el-button
 | 
			
		||||
      @click="$router.push({ path: '/nested/menu1/menu1-2/menu1-2-2' })"
 | 
			
		||||
    >
 | 
			
		||||
      跳转页内菜单(传path对象)
 | 
			
		||||
    </el-button>
 | 
			
		||||
    <el-link
 | 
			
		||||
      class="ml-4"
 | 
			
		||||
      href="https://router.vuejs.org/zh/guide/essentials/navigation.html#%E5%AF%BC%E8%88%AA%E5%88%B0%E4%B8%8D%E5%90%8C%E7%9A%84%E4%BD%8D%E7%BD%AE"
 | 
			
		||||
      target="_blank"
 | 
			
		||||
    >
 | 
			
		||||
      点击查看更多跳转方式
 | 
			
		||||
    </el-link>
 | 
			
		||||
 | 
			
		||||
    <el-divider />
 | 
			
		||||
    <el-button @click="$router.push({ name: 'Empty' })">
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										14
									
								
								src/views/tabs/params-detail.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/views/tabs/params-detail.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,14 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { useDetail } from "./hooks";
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: "TabParamsDetail"
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const { initToDetail, id } = useDetail();
 | 
			
		||||
initToDetail("params");
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div>{{ id }} - 详情页内容在此(params传参)</div>
 | 
			
		||||
</template>
 | 
			
		||||
@ -2,13 +2,13 @@
 | 
			
		||||
import { useDetail } from "./hooks";
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: "TabDetail"
 | 
			
		||||
  name: "TabQueryDetail"
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const { initToDetail, id } = useDetail();
 | 
			
		||||
initToDetail();
 | 
			
		||||
initToDetail("query");
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div>{{ id }} - 详情页内容在此</div>
 | 
			
		||||
  <div>{{ id }} - 详情页内容在此(query传参)</div>
 | 
			
		||||
</template>
 | 
			
		||||
@ -94,10 +94,10 @@ export interface RouteChildrenConfigsTable {
 | 
			
		||||
      /** 离场动画 */
 | 
			
		||||
      leaveTransition?: string;
 | 
			
		||||
    };
 | 
			
		||||
    // 是否不添加信息到标签页,(默认`false`)
 | 
			
		||||
    hiddenTag?: boolean;
 | 
			
		||||
    /** 动态路由可打开的最大数量 `可选` */
 | 
			
		||||
    dynamicLevel?: number;
 | 
			
		||||
    /** 刷新重定向(用于未开启标签页缓存,刷新页面获取不到动态`title`)`可选` */
 | 
			
		||||
    refreshRedirect?: string;
 | 
			
		||||
  };
 | 
			
		||||
  /** 子路由配置项 */
 | 
			
		||||
  children?: Array<RouteChildrenConfigsTable>;
 | 
			
		||||
 | 
			
		||||
@ -38,7 +38,7 @@ export default defineConfig({
 | 
			
		||||
  shortcuts: {
 | 
			
		||||
    "bg-dark": "bg-bg_color",
 | 
			
		||||
    "wh-full": "w-full h-full",
 | 
			
		||||
    "cp-on": "cursor-pointer outline-none",
 | 
			
		||||
    "flex-wrap": "flex flex-wrap",
 | 
			
		||||
    "flex-c": "flex justify-center items-center",
 | 
			
		||||
    "flex-ac": "flex justify-around items-center",
 | 
			
		||||
    "flex-bc": "flex justify-between items-center",
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user