美文网首页
一个vuex+vue-router实现的动态树形菜单

一个vuex+vue-router实现的动态树形菜单

作者: howtor | 来源:发表于2019-05-27 17:38 被阅读0次

    所谓动态菜单,就是菜单数据从后台加载,前端接收到的是一个JSON,前端代码解析后渲染相应的菜单信息,相应的路由也应该是动态加载的。

    TreeDetail.vue

    <template>
        <!--<h3>-->
        <div>
            <list-content :resourceId="resourceId"
                          :resourceType="resourceType"
                          ref="listContent">
            </list-content>
        </div>
    
        <!--</h3>-->
    </template>
    <script>
    
        import listContent from '../contents/content.vue'
    
        export default {
            name: "TreeViewDetail",
            data() {
                return {
                    isLoadList: false,
                    currentRoute: this.$route.path,
                    resourceId: '',
                    resourceType: ''            };
            },
            created() {
    
            },
            mounted() {
                this.initList();
            },
            methods: {
                // 初始化列表
                initList() {
                    if (this.$route.path != '' && this.$route.path != null && this.$route.path != undefined) {
                        this.resourceId = this.$route.name;
                        this.resourceType = 'link';
    
    
                    }
                }
    
            },
            watch: {
                //监听路由,只要路由有变化(路径,参数等变化)都有执行下面的函数
                $route: {
                    handler: function(val, oldVal) {
                        this.resourceId = val.name;
                        this.resourceType = 'link';
                        this.currentRoute = val.name;
                    },
                    deep: true
                }
            },
            components : {
                listContent
            }
        };
    </script>
    <style scoped>
        h3 {
            margin-top: 10px;
            font-weight: normal;
        }
    </style>
    

    TreeView.vue

    <template>
        <div class="tree-view-menu">
            <Tree-view-item :menus='menus'></Tree-view-item>
        </div>
    </template>
    <script>
        import TreeViewItem from "./TreeViewItem.vue";
    
        export default {
            components: {
                TreeViewItem
            },
            mounted() {
              console.log(this.menus);
            },
            name: "TreeViewMenu",
            data() {
                return {
                    menus: this.$store.state.menusModule.menus
                };
            }
        };
    </script>
    <style scoped>
        .tree-view-menu {
            background-color: #404655;
            width: 100%;
            height: 100%;
            overflow-y: auto;
            overflow-x: hidden;
    
        }
        .tree-view-menu::-webkit-scrollbar {
            height: 6px;
            width: 6px;
        }
        .tree-view-menu::-webkit-scrollbar-trac {
            -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
            box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
        }
        .tree-view-menu::-webkit-scrollbar-thumb {
            background-color: #6e6e6e;
            outline: 1px solid #333;
        }
        .tree-view-menu::-webkit-scrollbar {
            height: 4px;
            width: 4px;
        }
        .tree-view-menu::-webkit-scrollbar-track {
            -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
            box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
        }
        .tree-view-menu::-webkit-scrollbar-thumb {
            background-color: #6e6e6e;
            outline: 1px solid #708090;
        }
    </style>
    

    TreeViewItem.vue

    <template>
        <div class="tree-view-item">
            <div class="level" :class="'level-'+ menu.resourceLevel" v-for="menu in menus" :key="menu.id">
                <div v-if="menu.resourceType === 'link'">
                    <router-link class="link" v-bind:to="menu.resourceUrl" @click.native="toggle(menu)">{{menu.resourceName}}</router-link>
                </div>
                <div v-if="menu.resourceType === 'button'">
                    <div class="button heading" :class="{selected: menu.selected,expand:menu.expanded}" @click="toggle(menu)">
                        <span class="ats-tree-node"
                                :title="handleTitleVisible(menu.resourceName)">{{menu.resourceName}}</span>
                        <!--{{menu.resourceName}}-->
                        <div class="icon">
                            <svg xmlns="http://www.w3.org/2000/svg" focusable="false" viewBox="0 0 24 24">
                                <path d="M8.59 16.34l4.58-4.59-4.58-4.59L10 5.75l6 6-6 6z "></path>
                            </svg>
                        </div>
                    </div>
                    <transition name="fade">
                        <div class="heading-children" v-show="menu.expanded" v-if="menu.childQueues">
                            <Tree-view-item :menus='menu.childQueues'></Tree-view-item>
                        </div>
                    </transition>
                </div>
            </div>
        </div>
    </template>
    <script>
        export default {
            name: "TreeViewItem",
            props: ["menus"],
            created() {
                this.$store.commit("firstInit", { url: this.$route.path });
            },
            mounted() {
    
            },
            data() {
              return {
                  treeData: [],
              }
            },
            methods: {
                toggle(menu) {
                    this.$store.commit("findParents", { menu });
                },
                //超长菜单名后面用...代替,并且加上title属性
                handleTitleVisible(title){
                    let titleLen = title.replace(/[^\x00-\xff]/g, '..').length;
                    if(titleLen>10){
                        return title;
                    }else{
                        return '';
                    }
                }
            }
        };
    </script>
    <style scoped>
    
        a {
            text-decoration: none;
            color: #333;
        }
    
        .level-1 {
            font-size: 20px;
            font-weight: bold;
        }
        .level-2 {
            font-size: 18px;
            font-weight: lighter;
        }
        .level-3 {
            font-size: 16px;
            font-weight: lighter;
        }
    
        .link {
            font-size: 14px;
            font-weight: lighter;
        }
        .button {
            /*选中最靠近标题的菜单时,避免背景色一致导致视觉上连在一起*/
            margin-bottom: 2px;
        }
    
        .link,
        .button {
            color:#ffffff;
            display: block;
            padding: 10px 15px;
            transition: background-color 0.2s ease-in-out 0s, color 0.3s ease-in-out 0.1s;
            -moz-user-select: none;
            -webkit-user-select: none;
            -ms-user-select: none;
            -khtml-user-select: none;
            user-select: none;
        }
        .button {
            position: relative;
        }
        .link:hover,
        .button:hover {
            color: #1976d2;
            /*color: #ffffff;*/
            /*background-color: #eee;*/
            cursor: pointer;
        }
        .icon {
            position: absolute;
            right: 0;
            display: inline-block;
            height: 24px;
            width: 24px;
            fill: currentColor;
            transition: -webkit-transform 0.15s;
            transition: transform 0.15s;
            transition: transform 0.15s, -webkit-transform 0.15s;
            transition-timing-function: ease-in-out;
        }
    
        .heading-children {
            padding-left: 14px;
            overflow: hidden;
        }
        .expand {
            display: block;
        }
        .collapsed {
            display: none;
        }
        .expand .icon {
            -webkit-transform: rotate(90deg);
            transform: rotate(90deg);
        }
        .selected {
            /*background-color: #0071ff;*/
            color: #0071ff;
            font-weight: bold;
        }
        .fade-enter-active {
            transition: all 0.5s ease 0s;
        }
        .fade-enter {
            opacity: 0;
        }
        .fade-enter-to {
            opacity: 1;
        }
        .fade-leave-to {
            height: 0;
        }
    
        .ats-tree-node{
            max-width: 130px;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
            display: inline-block;
        }
    </style>
    

    初始化菜单信息以及菜单点击相关逻辑menusModule.js

    let menus = toArray();
    
    function toArray() {
        var obj =  sessionStorage.getItem("menuJson");
        return JSON.parse(obj);
    }
    
    
    let resourceLevelNum = 1;
    let startExpand = []; // 保存刷新后当前要展开的菜单项
    function setExpand(source, resourceUrl) {
    
        let sourceItem = '';
        for (let i = 0; i < source.length; i++) {
            sourceItem = JSON.stringify(source[i]); // 把菜单项转为字符串
            // 查找当前 resourceUrl 所对应的子菜单属于哪一个祖先菜单,并且初始路径不为'/',否则会把菜单第一个选然后选中并展开
            if (sourceItem.indexOf(resourceUrl) > -1 && resourceUrl != '/') {
                // debugger;
                if (source[i].resourceType === 'button') { // 导航菜单为按钮
                    source[i].selected = true; // 设置选中高亮
                    source[i].expanded = true; // 设置为展开
                    startExpand.push(source[i]);
                    // 递归下一级菜单,以此类推
                    setExpand(source[i].childQueues, resourceUrl);
                }
                break;
            }
        }
    }
    
    const state = {
        menus,
        resourceLevelNum
    };
    const mutations = {
    
        findParents(state, payload) {
            if (payload.menu.resourceType === "button") {
                payload.menu.expanded = !payload.menu.expanded;
            } else if (payload.menu.resourceType === "link") {
                if (startExpand.length > 0) {
                    for (let i = 0; i < startExpand.length; i++) {
                        startExpand[i].selected = false;
                    }
                }
                startExpand = []; // 清空展开菜单记录项
                setExpand(state.menus, payload.menu.resourceUrl);
            };
        },
        firstInit(state, payload) {
            setExpand(state.menus, payload.url);
        },
        // 初始化state中的菜单信息
        initStateMenus(state, payload) {
            state.menus = payload.menus;
        }
    }
    export default {
        state,
        mutations
    };
    

    路由index.js

    // 只给初始默认路由
    import Vue from 'vue';
    import Router from 'vue-router';
    
    import welcome from '../components/welcome.vue';
    import error from '../components/error.vue';
    
    Vue.use(Router)
    export default new Router({
        linkActiveClass: 'selected',
        mode: 'history',
       
        routes: [{
            path: '/',
            name: 'welcome',
            component: welcome
        },
            {
                path: '/error',
                name: 'error',
                component: error
            }
        ]
    
    })
    

    Home.vue

    <template>
        <Layout>
            <div class="home-wrapper layout">
                <div class="content-wrapper">
                    <template v-if="showHome">
                        <div class="side-bar">
                            <Tree-view></Tree-view>
                        </div>
                        <div class="continer">
                            <router-view></router-view>
                        </div>
                    </template>
                </div>
            </div>
            <!--<Footer class="layout-footer-center">2011-2016 &copy; TalkingData</Footer>-->
        </Layout>
    </template>
    <script>
    
        import listContent from './contents/content.vue'
        import TreeView from "./tree_menu/TreeView.vue";
        import TreeDetail from '../components/tree_menu/TreeDetail.vue';
    
        export default {
    
    
            name: "Home",
            data() {
                return {
                    showHome: false,
                    menuArr:[], // 遍历菜单menus时的临时变量
                    dynamicRouters: [] // 动态路由数组
    
                }
            },
            created() {
                this.initMenuInfo();
            },
            mounted() {
    
            },
            methods: {
    
                refreshList : function () {
                    this.$refs.listContent.refreshListPage(this.resourceId);
                },
                // 初始化菜单
                initMenuInfo() {
                    // 如果本地已存在,则不去后台加载
                    if (sessionStorage.getItem('menuJson')) {
                        // 添加动态路由
                        this.addDynamicRouters(JSON.parse(sessionStorage.getItem('menuJson')));
                        this.showHome = true;
                    } else {
                        this.axios.get(this.GLOBAL.gatewayCsbSrc + '/listQueue/getAppMenu/1').then((response) => {
                            //本地缓存菜单信息字符串
                            sessionStorage.removeItem('menuJson');
                            sessionStorage.setItem('menuJson',JSON.stringify(response.data));
                            // 添加动态路由
                            this.addDynamicRouters(response.data);
                            // 初始化状态中的菜单信息,否则第一次加载menusModule#firstInit中的status.menus为null,会报错
                            // 只有通过store.commit才有效
                            this.$store.commit("initStateMenus", { menus:  response.data});
                            // this.$router.push("/");
                            this.showHome = true;
                        }).catch((response) => {
                            console.log(response);
                        })
                    }
                },
                // 动态添加路由
                addDynamicRouters(menuObj) {
                    // 组装需要渲染的动态路由
                    this.assemblyRoutersArr(menuObj, this.menuArr);
                    // 将404页面的路由加在最后,因为路由会从上往下匹配,上面的匹配不到最终会走到这里,匹配任意,进入404页面
                    let errorRouter = {
                        path:'*',
                        redirect: '/error'
                    };
                    // 添加动态路由
                    this.dynamicRouters.push(errorRouter);
                    this.$router.addRoutes(this.dynamicRouters);
                },
                // 递归遍历菜单json,将其中type未link的提取出来拼成需要渲染的路由数组
                assemblyRoutersArr(json, arr) {
                    if (json == undefined) {
                        return;
                    }
                    for (var i = 0; i < json.length; i++) {
                        var sonList = json[i].childQueues;
                        // 资源类型未link的,添加到路由中
                        if (json[i].resourceType == 'link') {
                            // 防止重复添加动态路由
                            if (this.containsRouter(json[i].resourceId.toString())) {
                                continue;
                            }
    
                            let routerObj = {
                                path: json[i].resourceUrl,
                                name: json[i].resourceId.toString(),
                                component: TreeDetail
    
                            };
    
                            this.dynamicRouters.push(routerObj);
                        }
                        if (sonList.length == 0) {
                            arr.push(json[i]);
                        } else {
                            this.assemblyRoutersArr(sonList, arr);
                        }
                    }
                },
    
                containsRouter(resourceId) {
                    let routers = this.dynamicRouters;
                    if (routers.length == 0) {
                        return false;
                    }
                    for (var i = 0; i < routers.length; i++) {
                        if (routers.name == resourceId) {
                            return true;
                        }
                    }
                    return false;
                }
            },
            components: {
                TreeView,
                listContent,
                TreeDetail
            }
    
        };
    </script>
    <style scoped>
        .side-bar {
            width: 180px;
            height: 100%;
            font-size: 14px;
    
        }
        .continer {
            margin-left: 10px;
            width: calc(100% - 200px);
            height:100%;
        }
    
        .home-wrapper {
            display : flex;
            flex-direction: column;
            flex:1;
            width:100%;
            height: 100%;
        }
    
        .content-wrapper {
            display : flex;
            flex-direction: row;
            width:100%;
            /*height: calc(100% - 60px);*/
            height:100%;
        }
    
        .layout{
            border: 1px solid #d7dde4;
            background: #ffffff;
            /*background-color: red;*/
            position: relative;
            border-radius: 4px;
            overflow: hidden;
            height: 100%;
        }
    
    </style>
    

    静态图效果:


    微信截图_20190527174725.png

    参考了网上开源的一个tree组件,原文找不到了。

    动态菜单需要考虑每次都去后台加载,都需要连接查询,所以后台做了优化,菜单json存到redis中。
    前端也做了优化,菜单加载后使用sessionStorage存储到本地,优先从本地加载,加载不到再发起后台请求。
    递归遍历菜单json后,同时将路由信息也渲染一下,this.$router.addRoutes(routerArr),参数是数组,不是对象。如果链接不在路由信息中跳到404页面,路由信息从上向下查找,所以404的通配路由信息要放在最后,也就是上面未匹配到,最后匹配进入通配路由中,所以在动态路由加载完后,单独再将该路由信息添加到最后。

    相关文章

      网友评论

          本文标题:一个vuex+vue-router实现的动态树形菜单

          本文链接:https://www.haomeiwen.com/subject/kcqktctx.html