Skip to content

TagsView

流程

注意

  1. 监听路由变化,组成用于渲染 tags 的数据源
  2. 创建 tags 组件,根据数据源渲染 tag,渲染出来的 tags 需要同时具备 2.1. 国际化 title 2.2 路由跳转

数据源

  • tags 的数据我们最好把它保存到 pinia 中。
bash

1. 保存数据:`appmain` 组件中进行

2. 展示数据:`tags` 组件中进行

步骤

在 constant 中新建常量

  • src/constant/index.js
js
// tags
export const TAGS_VIEW = "tagsView";

stores 创建 useTags

  • stores/tagsView.js
js
/**
 * @Author: jsopy
 * @Date: 2025-01-18 17:05:38
 * @LastEditTime: 2025-01-18 17:43:49
 * @FilePath: /admin/src/stores/tagsView.js
 * @Description: tagsView store
 * @
 */

import { TAGS_VIEW } from "@/constant/index.js";

import { getItem, setItem } from "@/utils/storage";

import { defineStore } from "pinia";

export const useTags = defineStore("tagsView", () => {
  // 设置tags源
  const tagsViewList = ref(getItem(TAGS_VIEW) || []);

  // 添加tags
  const addTagsViewList = (tag) => {
    const isFind = tagsViewList.value.find((item) => {
      return item.path === tag.path;
    });
    // 处理重复
    if (!isFind) {
      tagsViewList.value.push(tag);
      setItem(TAGS_VIEW, tagsViewList.value);
    }
  };

  // 为指定的tag修改title

  const changeTagsView = ({ index, tag }) => {
    tagsViewList.value[index] = tag;
    setItem(TAGS_VIEW, tagsViewList.value);
  };

  // 删除tagsview
  /**
   * 删除 tag
   * @param {type: 'other'||'right'||'index', index: index} payload
   */
  const removeTagsView = (payload) => {
    if (payload.type === "index") {
      tagsViewList.value.splice(payload.index, 1);
      return;
    } else if (payload.type === "other") {
      tagsViewList.value.splice(
        payload.index + 1,
        tagsViewList.value.length - payload.index + 1
      );
      tagsViewList.value.splice(0, payload.index);
    } else if (payload.type === "right") {
      tagsViewList.value.splice(
        payload.index + 1,
        tagsViewList.value.length - payload.index + 1
      );
    }
    setItem(TAGS_VIEW, tagsViewList.value);
  };
  return {
    tagsViewList,
    addTagsViewList,
    changeTagsView,
    removeTagsView,
  };
});

创建白名单

  • utils/tags.js
js
/**
 * @Author: jsopy
 * @Date: 2025-01-18 17:13:28
 * @LastEditTime: 2025-01-18 17:15:37
 * @FilePath: /admin/src/utils/tags.js
 * @Description:
 * @
 */
const whiteList = ["/login", "/import", "/404", "/401"];

/**
 * path 是否需要被缓存
 * @param {*} path
 * @returns
 */
export function isTags(path) {
  return !whiteList.includes(path);
}

Appmain 监听路由变化

  • appmain/index.vue
vue
<!--
 * @Author: jsopy
 * @Date: 2025-01-12 09:45:02
 * @LastEditTime: 2025-01-18 17:44:18
 * @FilePath: /admin/src/components/AppMain/index.vue
 * @Description:
 *
-->
<template>
  <div class="app-main">
    <router-view></router-view>
  </div>
</template>

<script setup>
import { isTags } from "@/utils/tags";

import { generateTitle } from "@/utils/i18n";

import { useTags } from "@/stores/tagsView";

import { useI18n } from "vue-i18n";

const { tagsViewList, addTagsViewList, changeTagsView, removeTagsView } =
  useTags();

const route = useRoute();

const i18n = useI18n();

/* 生成title */

const getTitle = (route) => {
  let title = "";
  if (!route.meta) {
    // 处理无 meta 的路由
    const pathArr = route.path.split("/");
    title = pathArr[pathArr.length - 1];
  } else {
    title = generateTitle(route.meta.title);
  }
  return title;
};

/* 监听路由变化 */

watch(
  route,
  (to, from) => {
    if (!isTags(to.path)) return;
    const { fullPath, meta, name, params, path, query } = to;
    addTagsViewList({
      fullPath,
      meta,
      name,
      params,
      path,
      query,
      title: getTitle(to),
    });
  },
  {
    immediate: true,
  }
);

/* 监听 国际化 */
watch(
  () => {
    return i18n.locale.value;
  },
  (newval, oldval) => {
    console.log("改变了");
    console.log(newval);
    tagsViewList.forEach((route, index) => {
      changeTagsView({
        index,
        tag: {
          ...route,
          title: getTitle(route),
        },
      });
    });
  }
);
</script>

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

创建组件

  • components/TagsView/index.vue
vue
<!--
 * @Author: jsopy
 * @Date: 2025-01-18 17:25:39
 * @LastEditTime: 2025-01-18 17:45:23
 * @FilePath: /admin/src/components/TagsView/index.vue
 * @Description:
 *
-->
<template>
  <div class="tags-view-container" v-if="tagsViewList != []">
    <router-link
      class="tags-view-item"
      :class="isActive(tag) ? 'active' : ''"
      :style="{
        backgroundColor: isActive(tag) ? bgcolor : '',
        borderColor: isActive(tag) ? bgcolor : '',
      }"
      v-for="(tag, index) in useTags().tagsViewList"
      :key="tag.fullPath"
      :to="{ path: tag.fullPath }"
    >
      <span
        :style="{
          color: isActive(tag) ? activetextcolor : defaultextcolor,
        }"
      >
        {{ tag.title }}</span
      >
      <span
        @click.prevent.stop="onCloseClick(index)"
        v-show="!isActive(tag)"
        style="color: black; font-size: 12px"
      >
        x
      </span>
    </router-link>
  </div>
</template>

<script setup>
import { useTags } from "@/stores/tagsView";

import { useRoute } from "vue-router";

import { generateTitle } from "@/utils/i18n";

import { useZhuTiStore } from "@/stores/ZhuTi";

const { tagsViewList, addTagsViewList, changeTagsView, removeTagsView } =
  useTags();
/* 获取 变量 */
const ZhuTistore = useZhuTiStore();
const bgcolor = ref(ZhuTistore.bgcolor);
const defaultextcolor = ref(ZhuTistore.defaultTextColor);
const activetextcolor = ref(ZhuTistore.activeTextColor);
watch(
  () => {
    return ZhuTistore.bgcolor;
  },
  (newval, oldval) => {
    bgcolor.value = newval;
  }
);

watch(
  () => {
    return ZhuTistore.defaultTextColor;
  },
  (newval, old) => {
    defaultextcolor.value = newval;
  }
);

watch(
  () => {
    return ZhuTistore.activeTextColor;
  },
  (newval, oldval) => {
    activetextcolor.value = newval;
  }
);
const route = useRoute();
/**
 * 是否被选中
 */
const isActive = (tag) => {
  return tag.path === route.path;
};

/**
 * 关闭 tag 的点击事件
 */
const onCloseClick = (index) => {
  removeTagsView({ type: "index", index });
};
</script>

<style lang="scss" 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, 0.12), 0 0 3px 0 rgba(0, 0, 0, 0.04);
  .tags-view-item {
    display: inline-block;
    position: relative;
    cursor: pointer;
    height: 26px;
    line-height: 26px;
    border: 1px solid #d8dce5;
    color: #495060;
    background: #fff;
    padding: 0 8px;
    font-size: 12px;
    margin-left: 5px;
    margin-top: 4px;
    &:first-of-type {
      margin-left: 15px;
    }
    &:last-of-type {
      margin-right: 15px;
    }
    &.active {
      color: #fff;
      &::before {
        content: "";
        background: #fff;
        display: inline-block;
        width: 8px;
        height: 8px;
        border-radius: 50%;
        position: relative;
        margin-right: 4px;
      }
    }
    // close 按钮
    .el-icon-close {
      width: 16px;
      height: 16px;
      line-height: 10px;
      vertical-align: 2px;
      border-radius: 50%;
      text-align: center;
      transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
      transform-origin: 100% 50%;
      &:before {
        transform: scale(0.6);
        display: inline-block;
        vertical-align: -3px;
      }
      &:hover {
        background-color: #b4bccc;
        color: #fff;
      }
    }
  }
}
</style>

引入组件

  • src/layouts/default.vue
vue
<!--
 * @Author: jsopy
 * @Date: 2025-01-08 10:29:01
 * @LastEditTime: 2025-01-18 17:44:52
 * @FilePath: /admin/src/layouts/default.vue
 * @Description:
 *
-->
<template>
  <div class="app-wrapper">
    <!--左侧-->
    <SideBar id="guide-sidebar" class="sidebarleft"></SideBar>
    <!--左侧-->
    <!--右侧-->
    <div class="main-containerall">
      <div class="fixed-header">
        <!--顶部的navbar-->
        <NavBar></NavBar>
        <!--tags-->
        <TagsView></TagsView>
        <!--tags-->
      </div>
      <!--内容区域-->
      <div class="content">
        <AppMain></AppMain>
      </div>

      <!--内容区域-->
    </div>
    <!--右侧-->
  </div>
</template>

<script setup>
import { useCollapse } from "@/stores/sidebaropen";

const sidebarwidth = computed(() => {
  return useCollapse().sidebarOpened ? "64px" : "210px";
});
</script>

<style lang="scss" scoped>
.app-wrapper {
  display: flex;
  min-height: 100vh;
  width: 100%;
  flex-flow: row nowrap;
  .sidebarleft {
    flex: 0 0 v-bind(sidebarwidth);
    min-height: 100vh;
    background: #{$menuBg};
  }
  .main-containerall {
    flex: 1;
    display: flex;
    min-height: 100vh;
    flex-flow: column nowrap;
    .fixed-header {
      position: sticky;
      top: 0px;
      z-index: 10;
      flex: 0 0 105px;
      width: 100%;
    }
    .content {
      flex: 1;
      width: 100%;
    }
  }
}
</style>