zoukankan      html  css  js  c++  java
  • vue-element-admin学习笔记--权限加载及自定义布局(7)

    今天把TagsView部分代码看了一遍,动手搞了下,遇到的问题不多。主要是挺多前端的API不熟悉。

    TagsView

    没有使用Tag 标签,运用keep-alive和router-view的结合。根据vue-element-admin文档中介绍tags-view 维护了两个数组:

    • visitedViews : 用户访问过的页面 就是标签栏导航显示的一个个 tag 数组集合
    • cachedViews : 实际 keep-alive 的路由。可以在配置路由的时候通过 meta.noCache 来设置是否需要缓存这个路由 默认都缓存

    参考文档中设计,(文档地址)[https://panjiachen.gitee.io/vue-element-admin-site/zh/guide/essentials/tags-view.html#visitedviews-cachedviews]:
    由于目前 keep-alive 和 router-view 是强耦合的,而且查看文档和源码不难发现 keep-alive 的 include 默认是优先匹配组件的 name ,所以在编写路由 router 和路由对应的 view component 的时候一定要确保 两者的 name 是完全一致的。(切记 name 命名时候尽量保证唯一性 切记不要和某些组件的命名重复了,不然会递归引用最后内存溢出等问题)。一定要保证两着的名字相同,切记写重或者写错。默认如果不写 name 就不会被缓存,详情见issue

    缓存不适合场景

    目前缓存的方案对于某些业务是不适合的,比如文章详情页这种 /article/1 /article/2,他们的路由不同但对应的组件却是一样的,所以他们的组件 name 就是一样的,就如前面提到的,keep-alive的 include 只能根据组件名来进行缓存,所以这样就出问题了。目前有两种解决方案:

    • 不使用 keep-alive 的 include 功能 ,直接是用 keep-alive 缓存所有组件,这样子是支持前面所说的业务情况的。 前往@/layout/components/AppMain.vue文件下,移除include相关代码即可。当然直接使用 keep-alive 也是有弊端的,他并不能动态的删除缓存,你最多只能帮它设置一个最大缓存实例的个数 limit。相关 issue
    • 使用 localStorage 等浏览器缓存方案,自己进行缓存处理

    标签的左键切换到对应的菜单,中键判断如果是不是固定的标签就关闭,鼠标右键为刷新关闭其他等功能
    v-on

    • .stop - 调用 event.stopPropagation()。
    • .prevent - 调用 event.preventDefault()。
    • .capture - 添加事件侦听器时使用 capture 模式。
    • .self - 只当事件是从侦听器绑定的元素本身触发时才触发回调。
    • .{keyCode | keyAlias} - 只当事件是从特定键触发时才触发回调。
    • .native - 监听组件根元素的原生事件。
    • .once - 只触发一次回调。
    • .left - (2.2.0) 只当点击鼠标左键时触发。
    • .right - (2.2.0) 只当点击鼠标右键时触发。
    • .middle - (2.2.0) 只当点击鼠标中键时触发。
    • .passive - (2.3.0) 以 { passive: true } 模式添加侦听器

    右键事件绑定在contextmenu属性上
    当Tags过多超过页面时,通过滚轮的wheel事件来滚动加载TagsView的左右滚动

    参考vue-element-admin中源码,这里还是使用了Scrollbar这个组件。并且使用了VUE的内置组件slot。slot组件可以理解为父组件中使用子组件时,组建内涵盖的元素会被插入到子组件的slot位置。举例说明,有父组件root.vue,子组件child.vue,代码如下
    root.vue

    <div>
      <h1>父组件</h1>
      <child>
        <p>行1</p>
        <p>行2</p>
      </child>
    </div>
    

    child.vue

    <div>
      <h2>子组件</h2>
      <slot/>
    </div>
    

    最终结果

    <div>
      <h1>父组件</h1>
      <div>
        <h2>子组件</h2>
        <p>行1</p>
        <p>行2</p>
      </div>
    </div>
    

    在src>layouts>components下新建TagsView目录,并新建一个ScrollPane.vue。代码如下,样式省略

    <template>
      <el-scrollbar
        ref="scrollContainer"
        :vertical="false"
        class="scroll-container"
        @wheel.native.prevent="handleScroll"
      >
        <slot />
      </el-scrollbar>
    </template>
    
    <script>
    const tagAndTagSpacing = 4; // tagAndTagSpacing
    
    export default {
      name: "ScrollPane",
      data() {
        return {
          left: 0
        };
      },
      computed: {
        scrollWrapper() {
          return this.$refs.scrollContainer.$refs.wrap;
        }
      },
      methods: {
        handleScroll(e) {
          const eventDelta = e.wheelDelta || -e.deltaY * 40;
          const $scrollWrapper = this.scrollWrapper;
          $scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4;
        },
        moveToTarget(currentTag) {
          const $container = this.$refs.scrollContainer.$el;
          const $containerWidth = $container.offsetWidth;
          const $scrollWrapper = this.scrollWrapper;
          const tagList = this.$parent.$refs.tag;
    
          let firstTag = null;
          let lastTag = null;
    
          // find first tag and last tag
          if (tagList.length > 0) {
            firstTag = tagList[0];
            lastTag = tagList[tagList.length - 1];
          }
    
          if (firstTag === currentTag) {
            $scrollWrapper.scrollLeft = 0;
          } else if (lastTag === currentTag) {
            $scrollWrapper.scrollLeft =
              $scrollWrapper.scrollWidth - $containerWidth;
          } else {
            // find preTag and nextTag
            const currentIndex = tagList.findIndex(item => item === currentTag);
            const prevTag = tagList[currentIndex - 1];
            const nextTag = tagList[currentIndex + 1];
    
            // the tag's offsetLeft after of nextTag
            const afterNextTagOffsetLeft =
              nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + tagAndTagSpacing;
    
            // the tag's offsetLeft before of prevTag
            const beforePrevTagOffsetLeft =
              prevTag.$el.offsetLeft - tagAndTagSpacing;
    
            if (
              afterNextTagOffsetLeft >
              $scrollWrapper.scrollLeft + $containerWidth
            ) {
              $scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth;
            } else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
              $scrollWrapper.scrollLeft = beforePrevTagOffsetLeft;
            }
          }
        }
      }
    };
    </script>
    

    因为配置了国际化, 需要转换一下自定义的显示,所以要新建了一个工具的方法,用来转换。在src>utils新建i18n.js,代码如下

    // translate router.meta.title, be used in breadcrumb sidebar tagsview
    export function generateTitle(title) {
      const hasKey = this.$te("route." + title);
    
      if (hasKey) {
        // $t :this method from vue-i18n, inject in @/lang/index.js
        const translatedTitle = this.$t("route." + title);
    
        return translatedTitle;
      }
      return title;
    }
    

    按照vue-element-admin的思路,Tags都是router-link,而鼠标右键的事件绑定了页面可见性。同时要看route中是否配置了Affix属性,如果配置了则不会被删除,Tag就会固定。而针对Tag的操作,均在vuex中完成。在src>store>modules下新建一个tagsView.js,代码如下,并注册相关代码。同时在getters.js中配置visitedViews和cachedViews的获取。
    tagsView.js

    const state = {
      visitedViews: [],
      cachedViews: []
    };
    
    const mutations = {
      ADD_VISITED_VIEW: (state, view) => {
        if (state.visitedViews.some(v => v.path === view.path)) return;
        state.visitedViews.push(
          Object.assign({}, view, {
            title: view.meta.title || "no-name"
          })
        );
      },
      ADD_CACHED_VIEW: (state, view) => {
        if (state.cachedViews.includes(view.name)) return;
        if (!view.meta.noCache) {
          state.cachedViews.push(view.name);
        }
      },
    
      DEL_VISITED_VIEW: (state, view) => {
        for (const [i, v] of state.visitedViews.entries()) {
          if (v.path === view.path) {
            state.visitedViews.splice(i, 1);
            break;
          }
        }
      },
      DEL_CACHED_VIEW: (state, view) => {
        const index = state.cachedViews.indexOf(view.name);
        index > -1 && state.cachedViews.splice(index, 1);
      },
    
      DEL_OTHERS_VISITED_VIEWS: (state, view) => {
        state.visitedViews = state.visitedViews.filter(v => {
          return v.meta.affix || v.path === view.path;
        });
      },
      DEL_OTHERS_CACHED_VIEWS: (state, view) => {
        const index = state.cachedViews.indexOf(view.name);
        if (index > -1) {
          state.cachedViews = state.cachedViews.slice(index, index + 1);
        } else {
          // if index = -1, there is no cached tags
          state.cachedViews = [];
        }
      },
    
      DEL_ALL_VISITED_VIEWS: state => {
        // keep affix tags
        const affixTags = state.visitedViews.filter(tag => tag.meta.affix);
        state.visitedViews = affixTags;
      },
      DEL_ALL_CACHED_VIEWS: state => {
        state.cachedViews = [];
      },
    
      UPDATE_VISITED_VIEW: (state, view) => {
        for (let v of state.visitedViews) {
          if (v.path === view.path) {
            v = Object.assign(v, view);
            break;
          }
        }
      }
    };
    
    const actions = {
      addView({ dispatch }, view) {
        dispatch("addVisitedView", view);
        dispatch("addCachedView", view);
      },
      addVisitedView({ commit }, view) {
        commit("ADD_VISITED_VIEW", view);
      },
      addCachedView({ commit }, view) {
        commit("ADD_CACHED_VIEW", view);
      },
    
      delView({ dispatch, state }, view) {
        return new Promise(resolve => {
          dispatch("delVisitedView", view);
          dispatch("delCachedView", view);
          resolve({
            visitedViews: [...state.visitedViews],
            cachedViews: [...state.cachedViews]
          });
        });
      },
      delVisitedView({ commit, state }, view) {
        return new Promise(resolve => {
          commit("DEL_VISITED_VIEW", view);
          resolve([...state.visitedViews]);
        });
      },
      delCachedView({ commit, state }, view) {
        return new Promise(resolve => {
          commit("DEL_CACHED_VIEW", view);
          resolve([...state.cachedViews]);
        });
      },
    
      delOthersViews({ dispatch, state }, view) {
        return new Promise(resolve => {
          dispatch("delOthersVisitedViews", view);
          dispatch("delOthersCachedViews", view);
          resolve({
            visitedViews: [...state.visitedViews],
            cachedViews: [...state.cachedViews]
          });
        });
      },
      delOthersVisitedViews({ commit, state }, view) {
        return new Promise(resolve => {
          commit("DEL_OTHERS_VISITED_VIEWS", view);
          resolve([...state.visitedViews]);
        });
      },
      delOthersCachedViews({ commit, state }, view) {
        return new Promise(resolve => {
          commit("DEL_OTHERS_CACHED_VIEWS", view);
          resolve([...state.cachedViews]);
        });
      },
    
      delAllViews({ dispatch, state }, view) {
        return new Promise(resolve => {
          dispatch("delAllVisitedViews", view);
          dispatch("delAllCachedViews", view);
          resolve({
            visitedViews: [...state.visitedViews],
            cachedViews: [...state.cachedViews]
          });
        });
      },
      delAllVisitedViews({ commit, state }) {
        return new Promise(resolve => {
          commit("DEL_ALL_VISITED_VIEWS");
          resolve([...state.visitedViews]);
        });
      },
      delAllCachedViews({ commit, state }) {
        return new Promise(resolve => {
          commit("DEL_ALL_CACHED_VIEWS");
          resolve([...state.cachedViews]);
        });
      },
    
      updateVisitedView({ commit }, view) {
        commit("UPDATE_VISITED_VIEW", view);
      }
    };
    
    export default {
      namespaced: true,
      state,
      mutations,
      actions
    };
    
    

    TagsView组件的代码如下,大体思路已有,具体的方法也相对简单.

    <template>
      <div id="tags-view-container" class="tags-view-container">
        <scroll-pane ref="scrollPane" class="tags-view-wrapper">
          <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"
            @click.middle.native="!isAffix(tag) ? closeSelectedTag(tag) : ''"
            @contextmenu.prevent.native="openMenu(tag, $event)"
          >
            {{ generateTitle(tag.title) }}
            <span
              v-if="!isAffix(tag)"
              class="el-icon-close"
              @click.prevent.stop="closeSelectedTag(tag)"
            />
          </router-link>
        </scroll-pane>
    
        <ul
          v-show="visible"
          :style="{ left: left + 'px', top: top + 'px' }"
          class="contextmenu"
        >
          <li @click="refreshSelectedTag(selectedTag)">
            {{ $t("tagsView.refresh") }}
          </li>
          <li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
            {{ $t("tagsView.close") }}
          </li>
          <li @click="closeOthersTags">{{ $t("tagsView.closeOthers") }}</li>
          <li @click="closeAllTags(selectedTag)">{{ $t("tagsView.closeAll") }}</li>
        </ul>
      </div>
    </template>
    
    <script>
    import ScrollPane from "./ScrollPane";
    import { generateTitle } from "@/utils/i18n";
    import path from "path";
    
    export default {
      components: { ScrollPane },
      data() {
        return {
          visible: false,
          top: 0,
          left: 0,
          selectedTag: {},
          affixTags: []
        };
      },
      computed: {
        visitedViews() {
          return this.$store.state.tagsView.visitedViews;
        },
        routes() {
          return this.$store.state.permission.routes;
        }
      },
      watch: {
        $route() {
          this.addTags();
          this.moveToCurrentTag();
        },
        visible(value) {
          if (value) {
            document.body.addEventListener("click", this.closeMenu);
          } else {
            document.body.removeEventListener("click", this.closeMenu);
          }
        }
      },
      mounted() {
        this.initTags();
        this.addTags();
      },
      methods: {
        generateTitle, // generateTitle by vue-i18n
        isActive(route) {
          return route.path === this.$route.path;
        },
        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 = this.filterAffixTags(route.children, route.path);
              if (tempTags.length >= 1) {
                tags = [...tags, ...tempTags];
              }
            }
          });
          return tags;
        },
        initTags() {
          const affixTags = (this.affixTags = this.filterAffixTags(this.routes));
          for (const tag of affixTags) {
            // Must have tag name
            if (tag.name) {
              this.$store.dispatch("tagsView/addVisitedView", tag);
            }
          }
        },
        addTags() {
          const { name } = this.$route;
          if (name) {
            this.$store.dispatch("tagsView/addView", this.$route);
          }
          return false;
        },
        moveToCurrentTag() {
          const tags = this.$refs.tag;
          this.$nextTick(() => {
            for (const tag of tags) {
              if (tag.to.path === this.$route.path) {
                this.$refs.scrollPane.moveToTarget(tag);
                // when query is different then update
                if (tag.to.fullPath !== this.$route.fullPath) {
                  this.$store.dispatch("tagsView/updateVisitedView", this.$route);
                }
                break;
              }
            }
          });
        },
        refreshSelectedTag(view) {
          this.$store.dispatch("tagsView/delCachedView", view).then(() => {
            const { fullPath } = view;
            this.$nextTick(() => {
              this.$router.replace({
                path: "/redirect" + fullPath
              });
            });
          });
        },
        closeSelectedTag(view) {
          this.$store
            .dispatch("tagsView/delView", view)
            .then(({ visitedViews }) => {
              if (this.isActive(view)) {
                this.toLastView(visitedViews, view);
              }
            });
        },
        closeOthersTags() {
          this.$router.push(this.selectedTag);
          this.$store
            .dispatch("tagsView/delOthersViews", this.selectedTag)
            .then(() => {
              this.moveToCurrentTag();
            });
        },
        closeAllTags(view) {
          this.$store.dispatch("tagsView/delAllViews").then(({ visitedViews }) => {
            if (this.affixTags.some(tag => tag.path === view.path)) {
              return;
            }
            this.toLastView(visitedViews, view);
          });
        },
        toLastView(visitedViews, view) {
          const latestView = visitedViews.slice(-1)[0];
          if (latestView) {
            this.$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
              this.$router.replace({ path: "/redirect" + view.fullPath });
            } else {
              this.$router.push("/");
            }
          }
        },
        openMenu(tag, e) {
          const menuMinWidth = 105;
          const offsetLeft = this.$el.getBoundingClientRect().left; // container margin left
          const offsetWidth = this.$el.offsetWidth; // container width
          const maxLeft = offsetWidth - menuMinWidth; // left boundary
          const left = e.clientX - offsetLeft + 15; // 15: margin right
    
          if (left > maxLeft) {
            this.left = maxLeft;
          } else {
            this.left = left;
          }
    
          this.top = e.clientY;
          this.visible = true;
          this.selectedTag = tag;
        },
        closeMenu() {
          this.visible = false;
        }
      }
    };
    </script>
    

    最后需要注意的是如果需要缓存,还得得AppMain.vue中将缓存加上,示例如下:

    <keep-alive :include="cachedViews">
        <router-view :key="key" />
    </keep-alive>
    
  • 相关阅读:
    MVC session过期如何处理跳转
    MVC+EF更新数据库
    python 判断 windows 隐藏文件/系统文件
    字符编码简介
    Essential C++ 学习笔记02--Array/Vector 与指针
    Essential C++ 学习笔记01--基本语法
    mongodb 入门笔记
    何谓可分页和非分页内存
    Git使用
    _stdcall 函数 debug/release汇编代码区别
  • 原文地址:https://www.cnblogs.com/GYoungBean/p/12620443.html
Copyright © 2011-2022 走看看