he wei
2025-04-23 b9bd29a1a81f6f7de479e3cc3fdfe3d85fc660bf
src/layout/components/TagsView/index.vue
@@ -1,114 +1,79 @@
<template>
  <div id="tags-view-container" class="tags-view-container">
    <scroll-pane ref="scrollPane" class="tags-view-wrapper" @scroll="handleScroll">
      <div class="hamburger-container" @click="toggleSidebar">
        <svg-icon icon-class="drag"/>
      </div>
      <router-link v-for="tag in visitedViews" :key="tag.path"
        :to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }" custom
        v-slot="{ navigate, isActive, isExactActive }" ref="tag">
        <span :class="['tags-view-item', isActive && 'router-link-active', isExactActive && 'router-link-exact-active', tag.meta.icon && 'hide-dot']"
          @click="navigate"
          @click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
          @contextmenu.prevent="openMenu(tag, $event)">
          <svg-icon v-if="tag.meta.icon" :icon-class="tag.meta.icon" />
          {{ tag.title }}
          <icon-close v-if="!isAffix(tag)" class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)" />
        </span>
      </router-link>
    </scroll-pane>
    <ul v-show="visible" :style="{ left: left + 'px', top: top + 'px' }" class="contextmenu">
      <li @click="refreshSelectedTag(selectedTag)">刷新</li>
      <li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">关闭</li>
      <li @click="closeOthersTags">关闭其它</li>
      <li @click="closeAllTags(selectedTag)">关闭全部</li>
    </ul>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted, nextTick, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { storeToRefs } from "pinia";
<script setup name="TagsView">
import { defineComponent } from 'vue';
import ScrollPane from './ScrollPane';
import ScrollPane from "./ScrollPane.vue";
// import Hamburger from "../Hamburger.vue";
// import { useMenuStore } from "@/stores/menu.js";
import { useTagsViewStore } from "@/store/tagsView.js";
import { usePermissionStore } from '@/store/permission';
import path from 'path-browserify';
import useAppStore from '@/store/app';
import useTagsViewStore from '@/store/tagsView';
import usePromissionStore from '@/store/permission';
import { storeToRefs } from 'pinia';
import { Close as IconClose } from '@element-plus/icons-vue';
import SvgIcon from '@/components/SvgIcon/index.vue';
import { ref, watch, onMounted, nextTick } from 'vue';
import { useRoute, useRouter } from 'vue-router';
const route = useRoute();
const router = useRouter();
const appStore = useAppStore();
const tagsViewStore = useTagsViewStore();
const permissionStore = usePromissionStore();
const { sidebar } = storeToRefs(appStore);
const { visitedViews } = storeToRefs(tagsViewStore);
// import iconNav from "@/components/icons/iconNav.vue";
const permissionStore = usePermissionStore();
const { routes } = storeToRefs(permissionStore);
import { useAppStore } from '@/store/app';
const appStore = useAppStore();
const { toggleSidebar } = appStore;
const { addVisitedView, addView, updateVisitedView, delCachedView, delView, delOthersViews, delAllViews,  } = tagsViewStore;
const tagsViewStore = useTagsViewStore();
// const menuStroe = useMenuStore();
// const { isOpen } = storeToRefs(menuStroe);
const wraper = ref();
const { visitedViews, cachedViews } = storeToRefs(tagsViewStore);
const {
  addVisitedView,
  addView,
  delCachedView,
  delView,
  delOthersViews,
  delAllViews,
} = tagsViewStore;
const $router = useRouter();
const $route = useRoute();
const visible = ref(false);
const top = ref(0);
const left = ref(0);
const selectedTag = ref({});
const affixTags = ref([]);
const gData = reactive({ selectedTag: {}, affixTags: [] });
const tagRefs = reactive([]);
const scrollPane = ref();
const scrollPane = ref(null);
const tag = ref(null);
watch(
  () => route.path,
  () => {
    addTags();
    moveToCurrentTag();
  },
  {
    immediate: true
  }
);
watch(
  () => visible.value,
  value => {
    if (value) {
      document.body.addEventListener('click', closeMenu);
    } else {
      document.body.removeEventListener('click', closeMenu);
    }
  }
);
onMounted(() => {
  initTags();
watch($route, (val) => {
  console.log("val,", $route, "=============");
  addTags();
});
function isCurrentRoute(_route) {
  return route.path === _route.path;
function isActive(route) {
  return route.path === $route.path;
}
function isAffix(tag) {
  return tag.meta && tag.meta.affix;
}
function filterAffixTags(_routes, basePath = '/') {
function resolvePath(base, ...parts) {
  return base + (base.endsWith("/") ? "" : "/") + parts.join("/");
}
function filterAffixTags(_routes, basePath = "/") {
  let tags = [];
  _routes.forEach(route => {
    if (route.meta && route.meta.affix) {
      const tagPath = path.resolve(basePath, route.path);
  _routes.forEach((_route) => {
    if (_route.meta && _route.meta.affix) {
      // const tagPath = resolvePath(basePath, route.path);
      // const tagPath = resolvePath(basePath, route.path);
      const tagPath = path.resolve(basePath, _route.path);
      // const tagPath = _route.path;
      tags.push({
        fullPath: tagPath,
        path: tagPath,
        name: route.name,
        meta: { ...route.meta }
        name: _route.name,
        meta: {
          ..._route.meta,
        },
      });
    }
    if (route.children) {
      const tempTags = filterAffixTags(route.children, route.path);
    if (_route.children) {
      const tempTags = filterAffixTags(_route.children, _route.path);
      if (tempTags.length >= 1) {
        tags = [...tags, ...tempTags];
      }
@@ -116,175 +81,244 @@
  });
  return tags;
}
function initTags() {
  affixTags.value = filterAffixTags(routes.value);
  for (const tag of affixTags.value) {
  // gData.affixTags.length = 0;
  // gData.affixTags.push(...filterAffixTags(routes.value));
  gData.affixTags = filterAffixTags(routes.value);
  for (const tag of gData.affixTags) {
    // Must have tag name
    if (tag.name) {
      addVisitedView(tag);
    }
  }
}
function addTags() {
  const { name } = route;
  const { name } = $route;
  if (name) {
    addView(route);
    addView($route);
  }
  return false;
}
function moveToCurrentTag() {
  const tags = tag.value;
  nextTick(() => {
    for (const tag of tags) {
      if (tag.to.path === route.path) {
    for (const tag of tagRefs) {
      if (tag.to.path === $route.path) {
        scrollPane.value.moveToTarget(tag);
        // when query is different then update
        if (tag.to.fullPath !== route.fullPath) {
          updateVisitedView(route);
        if (tag.to.fullPath !== $route.fullPath) {
          // $store.dispatch("tagsView/updateVisitedView", $route);
          updateVisitedView($route);
        }
        break;
      }
    }
  });
}
function refreshSelectedTag(view) {
  closeMenu();
  delCachedView(view);
  const { fullPath } = view;
  const fullPath = view.fullPath;
  nextTick(() => {
    router.replace({
      path: '/redirect' + fullPath
    $router.replace({
      path: "/redirect" + fullPath,
    });
  })
  });
}
function closeSelectedTag(view) {
  closeMenu();
  delView(view);
  if (isActive(view)) {
    toLastView(visitedViews.value, view);
  }
}
function closeOthersTags() {
  router.push(selectedTag.value);
  delOthersViews(selectedTag.value);
  moveToCurrentTag();
  closeMenu();
  $router.push(gData.selectedTag);
  delOthersViews(gData.selectedTag);
    moveToCurrentTag();;
}
function closeAllTags(view) {
  closeMenu();
  delAllViews();
  if (affixTags.value.some(tag => tag.path === view.path)) {
    if (gData.affixTags.some((tag) => tag.path === view.path)) {
    return;
  }
  toLastView(visitedViews.value, view);
}
function toLastView(_visitedViews, view) {
  const latestView = _visitedViews.slice(-1)[0];
function toLastView(visitedViews, view) {
  const latestView = visitedViews.slice(-1)[0];
  if (latestView) {
    router.push(latestView.fullPath);
    $router.push(latestView.fullPath);
  } else {
    // now the default is to redirect to the home page if there is no tags-view,
    // you can adjust it according to your needs.
    if (view.name === 'Dashboard') {
    if (view.name === "home") {
      // to reload home page
      router.replace({ path: '/redirect' + view.fullPath });
      $router.replace({
        path: "/redirect" + view.fullPath,
      });
    } else {
      router.push('/');
      $router.push("/");
    }
  }
}
function openMenu(tag, e) {
  const menuMinWidth = 105;
  const offsetLeft = tag.getBoundingClientRect().left; // container margin left
  const offsetWidth = tag.offsetWidth; // container width
  const offsetLeft = wraper.value.getBoundingClientRect().left; // container margin left
  const offsetWidth = wraper.value.offsetWidth; // container width
  const maxLeft = offsetWidth - menuMinWidth; // left boundary
  const _left = e.clientX - offsetLeft + 15; // 15: margin right
  let _left = e.clientX - offsetLeft + 15; // 15: margin right
  if (left > maxLeft) {
  if (_left > maxLeft) {
    left.value = maxLeft;
  } else {
    left.value = _left;
  }
  top.value = e.clientY;
  top.value = 10;
  visible.value = true;
  selectedTag.value = tag;
  gData.selectedTag = tag;
}
function closeMenu() {
  visible.value = false;
}
function handleScroll() {
  closeMenu();
}
// function toggle() {
//   isOpen.value = !isOpen.value;
// }
onMounted(() => {
  initTags();
  addTags();
});
</script>
<template>
  <div id="tags-view-container" class="tags-view-container" ref="wraper">
    <div class="el-menu-icon-wrapper" @click="toggleSidebar">
      <el-icon class="nav" :size="24">
        <!-- <icon-nav></icon-nav> -->
        <svg-icon icon-class="menu" />
      </el-icon>
    </div>
    <!-- <hamburger :is-active="menu.opened" @toggleClick="toggleSideBar" /> -->
    <scroll-pane
      ref="scrollPane"
      class="tags-view-wrapper"
      @scroll="handleScroll"
    >
      <!-- :tagRefs="tagRefs" -->
      <!-- tag="span" -->
        <!-- :ref="(el) => (tagRefs[idx] = el)" -->
      <!-- <router-link
        v-for="(tag, idx) in visitedViews"
        :key="tag.path"
        ref="tagRefs"
        :class="isActive(tag) ? 'active' : ''"
        :to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
        class="tags-view-item"
        @click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
        @contextmenu.prevent="openMenu(tag, $event)"
      >
        {{ tag.title }}
        <el-icon
          v-if="!isAffix(tag)"
          class="el-icon-close"
          @click.prevent.stop="closeSelectedTag(tag)"
          ><Close
        /></el-icon>
      </router-link> -->
      <router-link
        v-for="tag in visitedViews"
        :key="tag.path"
        :to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
        custom
        v-slot="{ navigate, isActive, isExactActive }"
        :ref="(el) => (tagRefs[idx] = el)"
      >
        <span
          :class="['tags-view-item', isActive && 'router-link-active', isExactActive && 'router-link-exact-active', tag.meta.icon && 'hide-dot']"
          @click="navigate"
          @click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
          @contextmenu.prevent="openMenu(tag, $event)"
        >
          <svg-icon v-if="tag.meta.icon" :icon-class="tag.meta.icon" />
          {{ tag.title }}
          <el-icon
            v-if="!isAffix(tag)"
            class="el-icon-close"
            @click.prevent.stop="closeSelectedTag(tag)"
            ><Close
          /></el-icon>
        </span>
      </router-link>
    </scroll-pane>
    <ul
      v-show="visible"
      :style="{ left: left + 'px', top: top + 'px' }"
      class="contextmenu"
    >
      <li @click="refreshSelectedTag(gData.selectedTag)">刷新</li>
      <li
        v-if="!isAffix(gData.selectedTag)"
        @click="closeSelectedTag(gData.selectedTag)"
      >
        关闭当前
      </li>
      <li @click="closeOthersTags">关闭其他</li>
      <li @click="closeAllTags(gData.selectedTag)">关闭所有</li>
    </ul>
  </div>
</template>
<style lang="less" scoped>
.tags-view-container {
  height: 34px;
  width: 100%;
  background: #fff;
  border-bottom: 1px solid #d8dce5;
  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);
  .hamburger-container {
    display: inline-block;
    height: 32px;
    line-height: 32px;
    padding: 0 16px;
    border-left: 1px solid var(--light-color);
    border-right: 1px solid var(--light-color);
  // padding-left: 48px;
  position: relative;
  display: flex;
  border: 1px solid #0ff;
  .el-menu-icon-wrapper {
    display: flex;
    align-items: center;
    padding: 0 8px;
    color: #FFFFFF;
    &:hover {
      cursor: pointer;
      color: #0ff;
    }
  }
  .nav {
    float: left;
  }
  :deep(.el-scrollbar__view) {
    display: flex;
    align-items: center;
    overflow-y: hidden;
  }
  .tags-view-wrapper {
    // width: calc(100% - 48px);
    // background: gray;
    .tags-view-item {
      user-select: none;
      display: inline-block;
      position: relative;
      cursor: pointer;
      height: 26px;
      line-height: 26px;
      border: 1px solid #d8dce5;
      color: #495060;
      background: #fff;
      padding: 0 8px;
      height: 40px;
      line-height: 40px;
      padding: 0 20px;
      font-size: 12px;
      margin-left: 5px;
      margin-top: 4px;
      &:first-of-type {
        margin-left: 15px;
      }
      &:last-of-type {
        margin-right: 15px;
      }
      &.router-link-exact-active {
        background-color: #42b983;
      background: transparent;
        color: #fff;
        border-color: #42b983;
        &::before {
          content: '';
          background: #fff;
          display: inline-block;
          width: 8px;
          height: 8px;
          border-radius: 50%;
          position: relative;
          margin-right: 2px;
      &:hover {
        color: #0ff;
        }
        &.hide-dot {
          &::before {
            width: 0;
            height: 0;
          }
        }
      &.active {
        background-color: #00feff;
        border-color: #00feff;
        color: #041f6c;
      }
    }
  }
@@ -300,7 +334,7 @@
    font-size: 12px;
    font-weight: 400;
    color: #333;
    box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3);
    box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3);
    li {
      margin: 0;
@@ -313,30 +347,19 @@
    }
  }
}
</style>
<style lang="less">
//reset element css of el-icon-close
.tags-view-wrapper {
  .tags-view-item {
    .link {
      padding: 0 2px;
    }
    .el-icon-close {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      width: 16px;
      height: 16px;
      padding: 4px;
      margin-bottom: -4px;
      border-radius: 50%;
      text-align: center;
      transition: all .3s cubic-bezier(.645, .045, .355, 1);
      transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
      transform-origin: 100% 50%;
      &:before {
        transform: scale(.6);
        display: inline-block;
        vertical-align: -3px;
      }
      &:hover {
        background-color: #b4bccc;