vue-router v-slot及contextmenu

本篇工作总结,如果对您有帮助,看下就行,半公开。请不要转载,当然我写的一般都是很基础的内容,也没什么转载的必要

最近写代码过程中遇到了好多的bug和警告,本来写的还不成气候,不太适合记录。可是五一假期实在是太近了,再不记,恐怕假期后会忘光光。所以还是简单记录下吧。代码可能会有错误或疏漏,如有发现,还请指正。

下面一段是废话,可以略过。

下面我写的后台管理系统的页面标签功能,就是点击导航栏出现页签,点击页签会切换页面。这个后台管理系统大部分的代码都是抄的(咳,借鉴??!)若依的vue2,vue3以及Arco Design(UI框架)的内容,所以可能有的人会看上去好眼熟的。一开始不知道也没查若依有vue3版本,后来知道了,发现适配的是vue 3.0。虽然vue 3.0挺好的,但是现在vue 3.2版本毕竟出来了,而且简化了composition API的写法,因此我还是打算继续攒一套自己的后台管理系统。本项目是vue3+vue-router4+pinia2+Arco Design UI等构成。

总结:

1、一定要注意各种属性的正确拼写,不然就会产生暴击。

2、router-link去掉tag标签,采用v-slot的写法,具体的请参照下方代码

3、这是一个警告:Extraneous non-emits event listeners (contextmenu,mouseup) were passed to component but could not be automatically inherited because component renders fragment or text root nodes. If the listener is intended to be a component custom event listener only, declare it using the "emits" option.

这里主要出了两个问题,我先总结下:一是:vue3 @click.属性=function,属性被移除了一些。二是router-link的@contextmenu.prevent也不生效,这个我是采用UI的右键事件和window监听右键事件的方式修改bug的。

4、这个问题是部署的时候出现的,访问首页的时候,页面空白,并且控制台一直在循环输出找不到refs的属性。于是开始苦逼的找原因,后来发现问题出在router-link标签的ref="tag"这里,再进一步锁定moveToCurrentTag方法。最后解决方式是将获取tags挪到了nextTick里。详见下方代码

下面是啰嗦的一些废话,可以跳过。

具体什么意思,自行百度翻译或者谷歌翻译或者自己翻译下吧。但我看这个的时候是有点崩的。开玩笑,我搜遍了整个项目都没有用mouseup方法!!!后来将@click.后面的关键字去掉了,(具体是哪个我忘了),然后mouseup的警告消失。这是因为vue3拿掉了一些关键字,具体的我只记得一个native,因为在vue3中获取。。。。。(后面的读API吧,我忘记了)

下方代码是vue2版本,可参考比对。

<router-link v-for="tag in visitedViews" ref="tag" :key="tag.path" :class="isActive(tag)?'active':''"
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }" tag="span" class="tags-view-item" :style="activeStyle(tag)"
@click.middle.native="!isAffix(tag)?closeSelectedTag(tag):''" @contextmenu.prevent.native="openMenu(tag,$event)">
   {{ setTitle (tag) }}
   <span v-if="!isAffix(tag)" class="el-icon-close"                 
   @click.prevent.stop="closeSelectedTag(tag)" />
</router-link>

接下来就是contextmenu,这个是鼠标右击事件。我遇到的问题就是@contextmenu.prevent无效,阻止不了浏览器的原始事件。当然如果您在使用的时候可以生效,就可以略过本文了。在网上搜了好久,知道有一个插件可以解决。但是我发现我抄的的代码中没有这个依赖。于是我就开始了为什么,为什么别人的可以,我不可以,为什么,为什么。然后开始google和百度,结果都是一份答案各种传抄。

最后的我发现我用的UI框架支持右键点击自定义菜单,于是我被救了。

很快,我就又被淹了。我发现我的右键菜单是有判断条件的,不一定要显示全部,但我的按钮又是循环出来的。于是我又开始了各种失败的尝试。其实这个一直都有一个方法,把右键菜单放进循环里也可,但是我不愿意这样做。即使可以增加判断条件,也不愿意的那种!!!,倔强girl在线不服。我就又想了想,我为什么不试下监听contextmenu事件呢,最后bug解决了。

<template>
  <div id="tags-view-container" class="tags-view-container">
    <scroll-pane ref="scrollPaneRef" class="tags-view-wrapper">
      <a-dropdown trigger="contextMenu" align-point>
        <div class="tags-view-wrapper">
          <router-link v-for="tag in visitedViews" ref="tag" v-slot="{ isActive, navigate }" :key="tag.path" custom :to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }">
            <span class="arco-tag arco-tag-size-medium arco-tag-checked" :class="isActive ? 'active' : ''" @click="navigate()">
              <!-- <span v-if="!tagsView.isAffix(tag)" class="starBtn">
            <icon-star-fill />
          </span> -->
              {{ tagsView.setTitle(tag) }}
              <span v-if="!tagsView.isAffix(tag)" class="closeBtn" @click.prevent.stop="tagsView.closeSelectedTag(tag)">
                <icon-close-circle />
              </span>
            </span>
          </router-link>
        </div>
        <template #content>
          <a-doption @click="tagsView.refreshSelectedTag(tagsView.selectedTag)">刷新页面</a-doption>
          <a-doption v-if="!tagsView.isAffix(tagsView.selectedTag)" @click="tagsView.closeSelectedTag(tagsView.selectedTag)">关闭当前</a-doption>
          <a-doption @click="tagsView.closeOthersTags">关闭其他</a-doption>
          <a-doption v-if="!tagsView.isLastView()" @click="tagsView.closeRightTags">关闭右侧</a-doption>
          <a-doption @click="tagsView.closeAllTags(tagsView.selectedTag)">关闭所有</a-doption>
        </template>
      </a-dropdown>
    </scroll-pane>
  </div>
</template>

<script setup>
  import { reactive, ref, computed, watch, provide, nextTick, onMounted, onBeforeUnmount } from 'vue';
  import path from 'path';
  import { useRoute, useRouter } from 'vue-router';
  import { useTagStore, useRouterStore } from '@/store';
  import ScrollPane from './ScrollPane';
  import { getLog } from '@/api/tagsView';

  const $route = useRoute();
  const $router = useRouter();
  const tagStore = useTagStore();
  const routerStore = useRouterStore();
  const routeList = routerStore.routes;
  const visitedViews = computed(() => tagStore.visitedViews);
  const scrollPaneRef = ref(null);
  const tag = ref(null);
  const PARENT_PROVIDE = 'parentProvide';
  provide(PARENT_PROVIDE, tag);
  const tagsView = reactive({
    visible: false,
    selectedTag: {},
    affixTags: [],
    isActive(route) {
      return route.path === $route.path;
      // return route.fullPath === this.$route.fullPath
    },
    setTitle(tag) {
      if (tag.title.includes('新建策略')) {
        if (tag.query.type && tag.query.type == '编辑') {
          return '编辑策略-' + tag.query.id;
        } else {
          return tag.title;
        }
      }
      return tag.title;
    },
    isAffix(tag) {
      return tag.meta && tag.meta.affix;
    },
    filterAffixTags(routes, basePath = '/') {
      let tags = [];
      routes.forEach((route) => {
        if (route.meta && route.meta.affix) {
          const tagPath = path.resolve(basePath, route.path);
          tags.push({
            fullPath: tagPath,
            path: tagPath,
            name: route.name,
            meta: { ...route.meta },
          });
        }
        if (route.children) {
          const tempTags = tagsView.filterAffixTags(route.children, route.path);
          if (tempTags.length >= 1) {
            tags = [...tags, ...tempTags];
          }
        }
      });
      return tags;
    },
    initTags() {
      const affixTags = (tagsView.affixTags = tagsView.filterAffixTags(routeList));
      for (const tag of affixTags) {
        // Must have tag name
        if (tag.name) {
          tagStore.addVisitedView(tag);
        }
      }
    },
    addTags() {
      const { name } = $route;
      if (name) {
        getLog({ menuName: $route.meta.title }).then((res) => {
          if (res.code == 200) return;
        });
        tagStore.addView($route);
      }
      return false;
    },
    moveToCurrentTag() {
      nextTick(() => {
        const tags = tag.value;
        if (tags === null || tags === undefined) {
          return;
        }
        for (const tag of tags) {
          if (tag.to.path === $route.path) {
            scrollPaneRef.value.moveToTarget(tag);
            // when query is different then update
            if (tag.to.fullPath !== $route.fullPath) {
              tagStore.updateVisitedView($route);
            }
            break;
          }
        }
      });
    },
    refreshSelectedTag(view) {
      tagStore.delCachedView(view).then(() => {
        const { fullPath } = view;
        nextTick(() => {
          $router.replace({
            path: '/redirect' + fullPath,
          });
        });
      });
    },
    closeSelectedTag(view) {
      tagStore.delView(view).then(({ visitedViews }) => {
        if (tagsView.isActive(view)) {
          tagsView.toLastView(visitedViews, view);
        }
      });
    },
    closeRightTags() {
      tagStore.delRightTags(tagsView.selectedTag).then((visitedViews) => {
        // eslint-disable-next-line arrow-parens
        if (!visitedViews.find((i) => i.fullPath === $route.fullPath)) {
          tagsView.toLastView(visitedViews);
        }
      });
    },
    closeOthersTags() {
      $router.push(tagsView.selectedTag).catch(() => {});
      tagStore.delOthersViews(tagsView.selectedTag).then(() => {
        tagsView.moveToCurrentTag();
      });
    },
    closeAllTags(view) {
      tagStore.delAllViews().then(({ visitedViews }) => {
        // eslint-disable-next-line arrow-parens
        if (tagsView.affixTags.some((tag) => tag.path === $route.path)) {
          return;
        }
        tagsView.toLastView(visitedViews, view);
      });
    },
    toLastView(visitedViews, view) {
      const latestView = visitedViews.slice(-1)[0];
      if (latestView) {
        $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') {
          // to reload home page
          $router.replace({ path: '/redirect' + view.fullPath });
        } else {
          $router.push('/');
        }
      }
    },
    isLastView() {
      try {
        return tagsView.selectedTag.fullPath === visitedViews.value[visitedViews.value.length - 1].fullPath;
      } catch (err) {
        return false;
      }
    },
    openMenu(tag) {
      // eslint-disable-next-line arrow-parens
      tagsView.selectedTag = visitedViews.value.filter((item) => item.fullPath === tag)[0];
    },
    closeMenu() {
      tagsView.visible = false;
    },
    handleScroll() {
      tagsView.closeMenu();
    },
  });
  tagsView.initTags();
  tagsView.addTags();
  watch(
    () => $route.fullPath,
    () => {
      tagsView.addTags();
      tagsView.moveToCurrentTag();
    },
    { immediate: true }
  );
  watch(
    () => tagsView.visible,
    (val) => {
      if (val) {
        window.addEventListener('click', tagsView.closeMenu);
      } else {
        window.removeEventListener('click', tagsView.closeMenu);
      }
    },
    { immediate: true }
  );
  const menuRightClick = (e) => {
    const vueDom = e.target.__vueParentComponent;
    if (vueDom && vueDom.ctx && vueDom.ctx.to) {
      console.log('取消/监听鼠标右击事件,关闭功已实现,滚动条未测试,页数还不够');
      const fullPath = e.target.__vueParentComponent.ctx.to.fullPath;
      tagsView.openMenu(fullPath);
    }
  };
  onMounted(() => {
    window.addEventListener('contextmenu', menuRightClick, false);
    window.addEventListener('scroll', tagsView.handleScroll, false);
  });
  onBeforeUnmount(() => {
    window.removeEventListener('contextmenu', menuRightClick, false);
    window.removeEventListener('scroll', tagsView.handleScroll, false);
  });
</script>

<style lang="less" scoped>
  .tags-view-container {
    width: 100%;
    padding: 6px 0;
    background-color: var(--color-bg-2);
    box-shadow: 0 1px 3px 0 rgb(0 0 0 / 12%), 0 0 3px 0 rgb(0 0 0 / 12%);
    position: fixed;
    top: 60px;
    z-index: 100;
    .tags-view-wrapper {
      .arco-tag {
        margin-left: 10px;
        position: relative;
        cursor: pointer;
        &:first-of-type {
          margin-left: 15px;
        }
        &:last-of-type {
          margin-right: 15px;
        }
        &.active {
          background-color: rgb(var(--primary-6));
          color: var(--color-white);
          border-color: rgb(var(--primary-6));
          .starBtn {
            position: relative;
            left: 0;
            top: 0;
          }
          .closeBtn {
            position: relative;
            right: -2px;
          }
        }
      }
    }
    .contextmenu {
      margin: 0;
      background: #fff;
      z-index: 3000;
      position: absolute;
      list-style-type: none;
      padding: 5px 0;
      border-radius: 4px;
      font-size: 12px;
      font-weight: 400;
      color: #333;
      box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3);
      li {
        margin: 0;
        padding: 7px 16px;
        cursor: pointer;
        &:hover {
          background: #eee;
        }
      }
    }
  }
</style>

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值