瀏覽代碼

增加tag菜单功能

Akiraka 1 年之前
父節點
當前提交
d35219893a
共有 4 個文件被更改,包括 312 次插入0 次删除
  1. 197 0
      src/layout/components/TagView/TagView.vue
  2. 2 0
      src/layout/index.vue
  3. 83 0
      src/store/tagView.js
  4. 30 0
      src/utils/storage.js

+ 197 - 0
src/layout/components/TagView/TagView.vue

@@ -0,0 +1,197 @@
+<script setup>
+import { watch, ref, nextTick } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+import { useTagViewStore } from '@/store/tagView';
+import { onMounted } from 'vue';
+
+const store = useTagViewStore();
+const route = useRoute();
+const router = useRouter();
+
+const tagListRef = ref(null);
+const tagMore = ref(false);
+
+const checkTagListWidth = async () => {
+  await nextTick();
+  // 获取 tagList 宽度
+  const tagListWidth = tagListRef.value.offsetWidth;
+  // 获取 tagList 子元素实际宽度
+  const tagItemWidth = tagListRef.value.children[0].scrollWidth;
+  // 判断 tagList 宽度是否小于 tagItem 实际宽度
+  if (tagListWidth < tagItemWidth) {
+    // 设置 tagMore 为 true
+    tagMore.value = true;
+    // 计算向右偏移宽度
+    const rightOffset = tagListRef.value.children[0].offsetWidth - tagListRef.value.clientWidth;
+    tagListRef.value.children[0].style.right = rightOffset + 'px';
+  } else {
+    tagMore.value = false;
+  }
+}
+
+// Tag 标签上一页
+const handleTagPrev = () => {
+  let leftOffset = tagListRef.value.clientWidth - tagListRef.value.children[0].offsetWidth;
+
+  if (leftOffset < 0) {
+    leftOffset = 0;
+  }
+  tagListRef.value.children[0].style.right = leftOffset + 'px';
+}
+
+// Tag 标签下一页
+const handleTagNext = () => {
+  // 计算向右偏移宽度
+  const rightOffset = tagListRef.value.children[0].offsetWidth - tagListRef.value.clientWidth;
+  tagListRef.value.children[0].style.right = rightOffset + 'px';
+}
+
+// Tag 关闭事件
+const handleTagClose = (view, index) => {
+  // 判断是否为当前选中
+  if (view.path === route.path) {
+    // 判断是否为第一个
+    if (index !== 0 ) {
+      router.push(store.visitedViews[index - 1].path);
+    } else {
+      router.push(store.visitedViews[index + 1].path)
+    }
+  }
+  store.delVisitedViews(view);
+}
+
+const handleDelTag = (v) => {
+  switch (v) {
+    case 'left':
+      store.delLeftVisitedViews(route);
+      break;
+    case 'right':
+      store.delRightVisitedViews(route);
+      break;
+    case 'all':
+      store.delAllVisitedViews(route);
+      break;
+    default:
+      console.log('参数错误')
+  }
+}
+
+// 监听 store 数据变化
+watch(store.visitedViews, () => {
+  // 重新计算 tagList 宽度
+  checkTagListWidth();
+})
+
+// 监听路由变化
+watch(() => route.path, () => {
+  store.addVisitedViews(route)
+}, { immediate: true })
+
+onMounted(() => {
+  checkTagListWidth()
+})
+
+</script>
+
+<template>
+  <div class="tag-view-main">
+    <div
+      class="tag-card"
+      >
+      <div class="tag-prev" v-if="tagMore" @click="handleTagPrev">
+        <icon-left :size="18"/>
+      </div>
+
+      <div class="tag-list" ref="tagListRef">
+          <transition-group name="tag-fade">
+            <a-tag
+              v-for="(view, index) in store.visitedViews"
+              :key="view.name"
+              :checked="view.meta.title === route.meta.title"
+              :color="view.meta.title === route.meta.title ? '#5d87ff' : ''"
+              @close="handleTagClose(view, index)"
+              :closable="store.visitedViews.length > 1"
+              checkable
+              :style="{ marginRight: '5px' }"
+              >
+              <router-link :to="view.path" class="tag-link">
+                  {{ view.title }}
+              </router-link>
+            </a-tag>
+          </transition-group>
+          <!-- <a-tag
+            v-for="name, index in tagListCount"
+            :key="index"
+            @close="() => tagListCount -= 1"
+            closable>
+            标题{{ name }}
+          </a-tag> -->
+
+      </div>
+
+      <div class="tag-next" v-if="tagMore" @click="handleTagNext">
+          <icon-right :size="18"/>
+      </div>
+    </div>
+
+<!--    <div class="tag-close">-->
+<!--      <a-dropdown @select="handleDelTag">-->
+<!--        <a-button>-->
+<!--          <template #icon>-->
+<!--            <icon-down />-->
+<!--          </template>-->
+<!--        </a-button>-->
+<!--        <template #content>-->
+<!--          <a-doption value="left">关闭左侧</a-doption>-->
+<!--          <a-doption value="right">关闭右侧</a-doption>-->
+<!--          <a-doption value="all">关闭全部</a-doption>-->
+<!--        </template>-->
+<!--      </a-dropdown>-->
+<!--    </div>-->
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.tag-view-main {
+  padding: 0 15px;
+  display: flex;
+  justify-content: space-between;
+  max-width: 100%;
+  min-width: 100%;
+  background-color: var(--color-bg-2);
+  .tag-card {
+    display: flex;
+    align-items: center;
+    height: 36px;
+    overflow: hidden;
+    .tag-list {
+      position: relative;
+      overflow-x: hidden;
+      transition: .3s ease-in-out;
+    }
+    .tag-prev, .tag-next{
+      width: 48px;
+      cursor: pointer;
+      text-align: center;
+    }
+  }
+
+}
+
+.tag-link {
+  color: inherit;
+}
+.tag-link:hover {
+  color: inherit;
+}
+
+.tag-fade-enter-active,
+.tag-fade-leave-active {
+  transition: opacity 0.3s ease-in-out;
+}
+
+.tag-fade-enter-from,
+.tag-fade-leave-to {
+  opacity: 0;
+}
+</style>

+ 2 - 0
src/layout/index.vue

@@ -6,6 +6,7 @@
     <a-layout>
       <a-layout-header>
         <Navbar :collapsed="collapsed" @on-collapse="onCollapse" />
+        <tag-view />
       </a-layout-header>
       <a-layout-content class="layout-content">
         <AppMain />
@@ -18,6 +19,7 @@
 import { ref } from 'vue';
 import { AppMain, Navbar } from './components';
 import Menu from './components/Menu/Menu.vue';
+import TagView from './components/TagView/TagView.vue';
 
 const collapsed = ref(false);
 const onCollapse = () => {

+ 83 - 0
src/store/tagView.js

@@ -0,0 +1,83 @@
+import { defineStore } from 'pinia';
+import { ref } from 'vue';
+import { sessionStorage } from '@/utils/storage';
+
+export const useTagViewStore = defineStore('tagView', () => {
+    const visitedViews = ref(sessionStorage.getItem('tagList') || []);
+    const cachedViews = ref([]);
+
+    function addVisitedViews(view) {
+        console.log(view.meta.title)
+
+        // Akiraka 20240228 限制标签数量
+        if (visitedViews.value.length >= 20) {
+          visitedViews.value.splice(0,1)
+        }
+
+        if (visitedViews.value.some(v => v.path === view.path || v.meta.title === view.meta.title)) return;
+
+        visitedViews.value.push(
+            Object.assign({}, view, {
+                title: view.meta.title || 'no-name'
+            })
+        )
+        sessionStorage.setItem('tagList', visitedViews.value);
+    }
+
+    function delVisitedViews(view) {
+        for (const [i, v] of visitedViews.value.entries()) {
+            if (v.path === view.path) {
+                visitedViews.value.splice(i, 1);
+                break;
+            }
+        }
+        sessionStorage.setItem('tagList', visitedViews.value);
+    }
+
+    function delAllVisitedViews(view) {
+        visitedViews.value = visitedViews.value.filter(v => v.path === view.path);
+        sessionStorage.setItem('tagList', visitedViews.value);
+    }
+
+    function delLeftVisitedViews(view) {
+        for (const [i, v] of visitedViews.value.entries()) {
+            if (v.path === view.path) {
+                visitedViews.value = visitedViews.value.slice(i);
+                break;
+            }
+        }
+        sessionStorage.setItem('tagList', visitedViews.value);
+    }
+    function delRightVisitedViews(view) {
+        for (const [i, v] of visitedViews.value.entries()) {
+            if (v.path === view.path) {
+                visitedViews.value = visitedViews.value.slice(0, i + 1);
+                break;
+            }
+        }
+        sessionStorage.setItem('tagList', visitedViews.value);
+    }
+
+    function addCachedViews(view) {
+        if (cachedViews.value.includes(view.name)) return;
+        if (!view.meta.noCache) cachedViews.value.push(view.name);
+    }
+
+    function delCachedViews(view) {
+        visitedViews.value = visitedViews.value.filter(v => v !== view.name);
+    }
+
+    function delCachedVisitedViews() {
+        cachedViews.value = [];
+    }
+
+    return {
+        visitedViews,
+        cachedViews,
+        addVisitedViews,
+        delVisitedViews,
+        delAllVisitedViews,
+        delLeftVisitedViews,
+        delRightVisitedViews
+    }
+})

+ 30 - 0
src/utils/storage.js

@@ -28,4 +28,34 @@ export const storage = {
   removeItem(key) {
     window.localStorage.removeItem(key);
   }
+}
+
+export const sessionStorage = {
+  getKeys() {
+    const keys = [];
+    for (let i = 0; i < window.sessionStorage.length; i++) {
+      keys.push(window.sessionStorage.key(i));
+    }
+
+    return keys;
+  },
+  setItem(key, val) {
+    if (typeof key !== 'string') {
+      key = key.toString();
+    }
+
+    if (key === undefined || key.trim().length === 0) throw new Error('key 参数不能为空或者undefined');
+
+    window.sessionStorage.setItem(key, JSON.stringify(val));
+  },
+  getItem(key) {
+    const val = window.sessionStorage.getItem(key);
+    return JSON.parse(val);
+  },
+  clearAllKeys() {
+    window.sessionStorage.clear();
+  },
+  removeItem(key) {
+    window.sessionStorage.removeItem(key);
+  }
 }