vue3 el-tabs 和 el-dropdown 结合实现 tagsView 标签导航

大致思路

所谓 tagsView 可以分成两部分来去看:

  1. tags
  2. view

那怎么分开看呢?

首先我们先来看 tags

所谓 tgas 指的是:位于 appmain 之上的标签

那么现在我们忽略掉 view,现在只有一个要求:

view 之上渲染这个 tag

仅看这一个要求,很简单吧。

views:

明确好了 tags 之后,我们来看 views

脱离了 tags 只看 views 就更简单了,所谓 views指的就是一个用来渲染组件的位置,就像我们之前的 Appmain 一样,只不过这里的 views 可能稍微复杂一点,因为它需要在渲染的基础上增加:

  1. 动画
  2. 缓存(数据的缓存)

这两个额外的功能。

加上这两个功能之后可能会略显复杂,但是 官网已经帮助我们处理了这个问题

所以 单看 views 也是一个很简单的功能。

那么接下来我们需要做的就是把 tagsview 合并起来而已。

那么明确好了原理之后,我们就来看 实现方案:

  1. 创建 tagsView 组件:用来处理 tags 的展示
  2. 处理基于路由的动态过渡,在 AppMain 中进行:用于处理 view 的部分

整个的方案就是这么两大部,但是其中我们还需要处理一些细节相关的,完整的方案为

  1. 监听路由变化,组成用于渲染 tags 的数据源
  2. 创建 tags 组件,根据数据源渲染 tag,渲染出来的 tags 需要同时具备
    1. 国际化 title (这里不考虑)
    2. 路由跳转
  1. 处理鼠标右键效果,根据右键处理对应数据源
  2. 处理基于路由的动态过渡

那么明确好了方案之后,接下来我们根据方案进行处理即可。

基本布局

这里我们会基于element-plus的Tabs 标签页组件以及dropdown联合进行封装开发

layout/components/tagsView/idnex.vue

<template>
  <div
    class="f-tag-list"
    :style="{ left: appStore.sidebar.opened == '1' ? '220px' : '64px' }"
  >
    <el-tabs
      v-model="editableTabsValue"
      type="card"
      closable
      @edit="handleTabsEdit"
      class="flex-1"
      style="min-width: 100px;"
    >
      <el-tab-pane
        v-for="item in editableTabs"
        :key="item.name"
        :label="item.title"
        :name="item.name"
      >
      </el-tab-pane>
    </el-tabs>

    <span class="tag-btn">
      <el-dropdown>
        <span class="el-dropdown-link">
          <el-icon>
            <arrow-down />
          </el-icon>
        </span>
        <template #dropdown>
          <el-dropdown-menu>
            <el-dropdown-item>Action 1</el-dropdown-item>
            <el-dropdown-item>Action 2</el-dropdown-item>
            <el-dropdown-item>Action 3</el-dropdown-item>
            <el-dropdown-item disabled>Action 4</el-dropdown-item>
            <el-dropdown-item divided>Action 5</el-dropdown-item>
          </el-dropdown-menu>
        </template>
      </el-dropdown>
    </span>
  </div>
</template>

<script setup>
import useAppStore from "@/store/modules/app";

const appStore = useAppStore();

let tabIndex = 2;
const editableTabsValue = ref("2");
const editableTabs = ref([
  {
    title: "Tab 1",
    name: "1",
    content: "Tab 1 content",
  },
  {
    title: "Tab 2",
    name: "2",
    content: "Tab 2 content",
  },
  {
    title: "Tab 2",
    name: "3",
    content: "Tab 2 content",
  },
  {
    title: "Tab 2",
    name: "4",
    content: "Tab 2 content",
  },
  {
    title: "Tab 2",
    name: "5",
    content: "Tab 2 content",
  },
  {
    title: "Tab 2",
    name: "6",
    content: "Tab 2 content",
  },
  {
    title: "Tab 2",
    name: "7",
    content: "Tab 2 content",
  },
  {
    title: "Tab 2",
    name: "8",
    content: "Tab 2 content",
  },
  {
    title: "Tab 2",
    name: "9",
    content: "Tab 2 content",
  },
  {
    title: "Tab 2",
    name: "10",
    content: "Tab 2 content",
  },
]);
const handleTabsEdit = (targetName, action) => {
  if (action === "add") {
    const newTabName = `${++tabIndex}`;
    editableTabs.value.push({
      title: "New Tab",
      name: newTabName,
      content: "New Tab content",
    });
    editableTabsValue.value = newTabName;
  } else if (action === "remove") {
    const tabs = editableTabs.value;
    let activeName = editableTabsValue.value;
    if (activeName === targetName) {
      tabs.forEach((tab, index) => {
        if (tab.name === targetName) {
          const nextTab = tabs[index + 1] || tabs[index - 1];
          if (nextTab) {
            activeName = nextTab.name;
          }
        }
      });
    }

    editableTabsValue.value = activeName;
    editableTabs.value = tabs.filter((tab) => tab.name !== targetName);
  }
};
</script>

<style scoped lang="scss">
:deep(.el-tooltip__trigger:focus-visible) {
  outline: unset !important;
}

:deep(.el-tabs__header) {
  margin-bottom: 0 !important;
}

:deep(.el-tabs--card > .el-tabs__header) {
  border-bottom: none !important;
}

:deep(.el-tabs__nav) {
  border: 0 !important;
}

:deep(.el-tabs--card > .el-tabs__header .el-tabs__item) {
  border-left: 0 !important;
  height: 32px !important;
  line-height: 32px !important;
  background-color: rgba(216,226,249, .3);
  @apply mx-1 rounded;
}

:deep(.el-tabs__nav-next), :deep( .el-tabs__nav-prev) {
    line-height: 32px;
    height: 32px;
}

:deep(.is-disabled) {
    cursor: not-allowed;
    @apply text-gray-300;
}

.f-tag-list {
  height: 44px;
  z-index: 100;
  width: 100%;
  background: #fff;
  -webkit-box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12),
    0 0 3px 0 rgba(0, 0, 0, 0.04);
  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 0 3px 0 rgba(0, 0, 0, 0.04);
  @apply bg-white flex items-center px-2;
}

.tag-btn {
  @apply bg-white rounded ml-auto flex items-center justify-center;
  height: 32px;
  width: 32px;
  -webkit-box-shadow: 0 0px 1px 0 rgba(0, 0, 0, 0.12),
    0 0 0px 0 rgba(0, 0, 0, 0.04);
  box-shadow: 0 px 1px 0 rgba(0, 0, 0, 0.12), 0 0 1px 0 rgba(0, 0, 0, 0.04);
}
</style>

同步路由和存储

layout/components/tagsView/index.vue

<template>
  <div
    class="f-tag-list"
    :style="{ left: appStore.sidebar.opened == '1' ? '220px' : '64px' }"
  >
    <el-tabs
      v-model="activeTags"
      type="card"
      @edit="handleTagsEdit"
      @tab-change="tabChangeEvent"
      class="flex-1"
      style="min-width: 100px"
    >
      <el-tab-pane
        v-for="item in tagsList"
        :key="item.path"
        :label="item.title"
        :name="item.path"
        :closable="item.path != '/dashboard'"
      >
      </el-tab-pane>
    </el-tabs>

    <span class="tag-btn">
      <el-dropdown>
        <span class="el-dropdown-link">
          <el-icon>
            <arrow-down />
          </el-icon>
        </span>
        <template #dropdown>
          <el-dropdown-menu>
            <el-dropdown-item>Action 1</el-dropdown-item>
            <el-dropdown-item>Action 2</el-dropdown-item>
            <el-dropdown-item>Action 3</el-dropdown-item>
            <el-dropdown-item disabled>Action 4</el-dropdown-item>
            <el-dropdown-item divided>Action 5</el-dropdown-item>
          </el-dropdown-menu>
        </template>
      </el-dropdown>
    </span>
  </div>
</template>

<script setup>
import useAppStore from "@/store/modules/app";
import Cookies from "js-cookie";

const route = useRoute();
const router = useRouter();
const appStore = useAppStore();

const activeTags = ref(route.path);
const tagsList = ref([
  {
    title: "首页",
    path: "/dashboard",
  },
]);

// 初始化tags标签
function initTagsList() {
  let tagsInitList = Cookies.get('tagsList');
  if(tagsInitList) {
    tagsList.value = JSON.parse(tagsInitList);
  }
}
initTagsList()

// 添加标签导航
function addtags(tag) {
  // 判断是否已经存在tag
  let noTag = tagsList.value.findIndex((t) => t.path == tag.path) == -1;
  if (noTag) {
    tagsList.value.push(tag);
  }
  // 缓存
  Cookies.set("tagsList", JSON.stringify(tagsList.value));
}

// 监听路由变化
onBeforeRouteUpdate((to, from) => {
  activeTags.value = to.path;
  addtags({
    title: to.meta.title,
    path: to.path,
  });
});

// 标签栏点击事件
const handleTagsEdit = (targetName, action) => {
  console.log(action, targetName);
  if (action === "add") {
  } else if (action === "remove") {
  }
};

// 监听 tag 的切换
const tabChangeEvent = (name) => {
  activeTags.value = name;
  router.push(name)
};
</script>

<style scoped lang="scss">
  ...
</style>

关闭标签导航实现

当我们点击关闭标签导航的时候会自动切换到下一个;关闭下一个标签导航会自动切换到上一个

layout/components/tagsView/index.vue

<el-tabs
  v-model="activeTags"
  type="card"
++  @tab-remove="handleTagsRemoveEvent"
  @tab-change="tabChangeEvent"
  class="flex-1"
  style="min-width: 100px"
>

// 标签栏点击事件
const handleTagsRemoveEvent = (t) => {
  let tags = tagsList.value;
  let currentActiveTag = activeTags.value;

  // 如果关闭的是当前激活的标签
  if (currentActiveTag === t) {
    // 尝试找到下一个或上一个可激活的标签
    let nextTag;
    for (let i = 0; i < tags.length; i++) {
      if (tags[i].path === t) {
        nextTag = tags[i + 1] || tags[i - 1]; // 尝试找下一个或上一个标签
        break;
      }
    }

    // 如果没有找到可激活的标签(例如,只剩下一个标签且被关闭),可以选择默认路由
    if (!nextTag) {
      nextTag = tags[0]; // 或者其他默认逻辑
    }

    // 更新激活标签
    activeTags.value = nextTag.path;

    // 如果新激活的标签不是当前路由,则跳转
    if (nextTag.path !== route.path) {
      router.push(nextTag.path);
    }
  }

  // 过滤标签列表并缓存
  tagsList.value = tags.filter((tag) => tag.path !== t);
  Cookies.set("tagsList", JSON.stringify(tagsList.value));
};

关闭其他和关闭全部功能实现

layout/components/tagsView/index.vue

<span class="tag-btn">
  <el-dropdown @command="commandEvent">
    <span class="el-dropdown-link">
      <el-icon>
        <arrow-down />
      </el-icon>
    </span>
    <template #dropdown>
      <el-dropdown-menu>
        <el-dropdown-item command="clearOther">关闭其他</el-dropdown-item>
        <el-dropdown-item command="clearAll">全部关闭</el-dropdown-item>
      </el-dropdown-menu>
    </template>
  </el-dropdown>
</span>

// 关闭其他/关闭全部功能
const commandEvent = (val) => {
  if (val == "clearAll") {
    // 标签激活状态切换为 首页
    activeTags.value = "/dashboard";
    // 直接跳转到首页路由
    router.push("/dashboard");
    // 过滤 tags 数组,只剩下首页
    tagsList.value = [
      {
        title: "首页",
        path: "/dashboard",
      },
    ];
  } else if (val == "clearOther") {
    // 过滤只剩下首页和当前激活的tag
    tagsList.value = tagsList.value.filter(
      (tab) => tab.path == "/dashboard" || tab.path == activeTags.value
    );
  }
  Cookies.set("tagsList", JSON.stringify(tagsList.value));
};

优化逻辑

对于上面写的逻辑我们这里对其进行一个抽离封装成hooks

hooks/useTags.js

import Cookies from "js-cookie";

export function useTags() {
  const route = useRoute();
  const router = useRouter();

  const activeTags = ref(route.path);
  const tagsList = ref([
    {
      title: "首页",
      path: "/dashboard",
    },
  ]);

  // 初始化tags标签
  function initTagsList() {
    let tagsInitList = Cookies.get("tagsList");
    if (tagsInitList) {
      tagsList.value = JSON.parse(tagsInitList);
    }
  }
  initTagsList();

  // 添加标签导航
  function addtags(tag) {
    // 判断是否已经存在tag
    let noTag = tagsList.value.findIndex((t) => t.path == tag.path) == -1;
    if (noTag) {
      tagsList.value.push(tag);
    }
    // 缓存
    Cookies.set("tagsList", JSON.stringify(tagsList.value));
  }

  // 监听路由变化
  onBeforeRouteUpdate((to, from) => {
    activeTags.value = to.path;
    addtags({
      title: to.meta.title,
      path: to.path,
    });
  });

  // 监听 tag 的切换
  const tabChangeEvent = (name) => {
    activeTags.value = name;
    router.push(name);
  };

  // 标签栏点击事件
  const handleTagsRemoveEvent = (t) => {
    let tags = tagsList.value;
    let currentActiveTag = activeTags.value;

    // 如果关闭的是当前激活的标签
    if (currentActiveTag === t) {
      // 尝试找到下一个或上一个可激活的标签
      let nextTag;
      for (let i = 0; i < tags.length; i++) {
        if (tags[i].path === t) {
          nextTag = tags[i + 1] || tags[i - 1]; // 尝试找下一个或上一个标签
          break;
        }
      }

      // 如果没有找到可激活的标签(例如,只剩下一个标签且被关闭),可以选择默认路由
      if (!nextTag) {
        nextTag = tags[0]; // 或者其他默认逻辑
      }

      // 更新激活标签
      activeTags.value = nextTag.path;

      // 如果新激活的标签不是当前路由,则跳转
      if (nextTag.path !== route.path) {
        router.push(nextTag.path);
      }
    }

    // 过滤标签列表并缓存
    tagsList.value = tags.filter((tag) => tag.path !== t);
    Cookies.set("tagsList", JSON.stringify(tagsList.value));
  };

  // 关闭其他/关闭全部功能
  const commandEvent = (val) => {
    if (val == "clearAll") {
      // 标签激活状态切换为 首页
      activeTags.value = "/dashboard";
      // 直接跳转到首页路由
      router.push("/dashboard");
      // 过滤 tags 数组,只剩下首页
      tagsList.value = [
        {
          title: "首页",
          path: "/dashboard",
        },
      ];
    } else if (val == "clearOther") {
      // 过滤只剩下首页和当前激活的tag
      tagsList.value = tagsList.value.filter(
        (tab) => tab.path == "/dashboard" || tab.path == activeTags.value
      );
    }
    Cookies.set("tagsList", JSON.stringify(tagsList.value));
  };

  return {
    activeTags,
    tagsList,
    tabChangeEvent,
    handleTagsRemoveEvent,
    commandEvent,
  }
}

layout/components/tagsView/index.vue

<template>
  <div
    class="f-tag-list"
    :style="{ left: appStore.sidebar.opened == '1' ? '220px' : '64px' }"
  >
    <el-tabs
      v-model="activeTags"
      type="card"
      @tab-remove="handleTagsRemoveEvent"
      @tab-change="tabChangeEvent"
      class="flex-1"
      style="min-width: 100px"
    >
      <el-tab-pane
        v-for="item in tagsList"
        :key="item.path"
        :label="item.title"
        :name="item.path"
        :closable="item.path != '/dashboard'"
      >
      </el-tab-pane>
    </el-tabs>

    <span class="tag-btn">
      <el-dropdown @command="commandEvent">
        <span class="el-dropdown-link">
          <el-icon>
            <arrow-down />
          </el-icon>
        </span>
        <template #dropdown>
          <el-dropdown-menu>
            <el-dropdown-item command="clearOther">关闭其他</el-dropdown-item>
            <el-dropdown-item command="clearAll">全部关闭</el-dropdown-item>
          </el-dropdown-menu>
        </template>
      </el-dropdown>
    </span>
  </div>
</template>

<script setup>
import useAppStore from "@/store/modules/app";
import { useTags } from "@/hooks/useTags";

const appStore = useAppStore();
const {
  activeTags,
  tagsList,
  tabChangeEvent,
  handleTagsRemoveEvent,
  commandEvent,
} = useTags();
</script>

<style scoped lang="scss">
:deep(.el-tooltip__trigger:focus-visible) {
  outline: unset !important;
}

:deep(.el-tabs__header) {
  margin-bottom: 0 !important;
}

:deep(.el-tabs--card > .el-tabs__header) {
  border-bottom: none !important;
}

:deep(.el-tabs__nav) {
  border: 0 !important;
}

:deep(.el-tabs--card > .el-tabs__header .el-tabs__item) {
  border-left: 0 !important;
  height: 32px !important;
  line-height: 32px !important;
  background-color: rgba(216, 226, 249, 0.3);
  @apply mx-1 rounded;
}

:deep(.el-tabs__nav-next),
:deep(.el-tabs__nav-prev) {
  line-height: 32px;
  height: 32px;
}

:deep(.is-disabled) {
  cursor: not-allowed;
  @apply text-gray-300;
}

.f-tag-list {
  height: 44px;
  z-index: 100;
  width: 100%;
  background: #fff;
  -webkit-box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12),
    0 0 3px 0 rgba(0, 0, 0, 0.04);
  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 0 3px 0 rgba(0, 0, 0, 0.04);
  @apply bg-white flex items-center px-2;
}

.tag-btn {
  @apply bg-white rounded ml-auto flex items-center justify-center;
  height: 32px;
  width: 32px;
  -webkit-box-shadow: 0 0px 1px 0 rgba(0, 0, 0, 0.12),
    0 0 0px 0 rgba(0, 0, 0, 0.04);
  box-shadow: 0 px 1px 0 rgba(0, 0, 0, 0.12), 0 0 1px 0 rgba(0, 0, 0, 0.04);
}
</style>

到这里的话我们的tagsView布局和功能就开发完成了。

当然这里也可以自己手写一个,我这里给出一个手写的示例代码:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小白小白从不日白

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值