vue ---- tagsView

效果图
抱歉嘿嘿

文里的tagList和activeTag全部存sessionStorage里了, 现在想来没那个必要😂存那里和放假仓库里是一回事, 不刷新都在, 刷新就没......已然记不起当时为什么要这么地了...........这里的代码不想改了, 真有用到的那么地倒是也无大碍哈哈哈哈哈哈哈

功能:
  1. 点击跳转对应页面;
  2. 设置activeTag;
  3. 样式方面: 如果点击左数第二个时, 左数第一个没有完全露出, 让容器滚动使前一个tag露出; 右边同样(之前没有关注过, 写到这里才发现, 没有scrollRight和offsetRight啊😂);
  4. 右键小菜单: 位置跟着鼠标走(功能如图);
  5. 设置固定tag, 关闭全部功能为disabled状态;
实现

跳转以及其它本应该加在每个tag上的方法, 我写在了父容器上, 使用srcElement获取到当前元素. contextMenu也写在了父容器中, 定位使用了fixed, 根据鼠标位置来确定. .stop消除冒泡并且设置另外的方法进行占位.

假仓库: 新建一个js文件, 里面是vue实例. 抛出嘿嘿
一般用来保存用户信息及过滤完毕的路由和tags. 缺点和vuex一样, 刷新页面, 数据消失.

上课的时候老师说如果不是超大型项目没有必要使用vuex. 原因我忘却了....不过很多时候不是所有东西都需要储存在cookie或者storage里, 我们委实需要一个类似vuex的东西.

tag相关内容没有必要像token一样一直保存. 如果当前页面关闭, 它的信息就可以清除了. 但刷新页面时仓库数据消失但我们还是需要这些数据, 所以存在sessionStorage中是最为合适的😊

假仓库代码 ⬇️ (我有一个问题....原本想把constantRoutes给它export, tag.vue中使用, 但是别的页面可以, tag.vue一用直接页面都出不来了😭)

import Vue from 'vue';
import Cookie from "js-cookie";
import Layout from "@/components/layout";
import Router, {
    resetRouter
} from "@/router";

const constantRoutes = [{
    path: '/',
    redirect: '/home',
    component: Layout,
    children: [{
        path: 'home',
        fullPath: '/home',
        name: "views-home",
        meta: {
            title: '牛进喜冲'
        },
        eternalTag: true,
        icon: "fa-bandcamp",
        component: () => import('@/views/home.vue')
    }]
}, {
    path: '*',
    redirect: "/404"
}];

export default new Vue({
    data() {
        return {
            tagList: [],
            activeTag: sessionStorage.getItem('NJX_ACTIVE_TAG') || "views-home",
        }
    },
    methods: {
        // 初始化tagList
        initTagList() {
            // 利用递归取出constantRoutes中eternalTag值为true的数据
            let arr = [];
            let getEternal = (list) => {
                list.map(item => {
                    if(item.eternalTag) {
                        arr.push(item)
                    }
                    if(item.children) {
                        getEternal(item.children);
                    }
                })
            }
            this.tagList = sessionStorage.getItem('NJX_TAGS') ? JSON.parse(sessionStorage.getItem('NJX_TAGS')) : getEternal(constantRoutes);
        },
        // 保存tagList
        saveTags(list) {
            this.tagList = list;
            sessionStorage.setItem('NJX_TAGS', JSON.stringify(list));
        },
        // 移除tag ---- 右边的小❎
        removeTag(index) {
            // 是否是当前项
            let isCurrentActive = this.activeTag === this.tagList[index].name;
            // 删
            this.tagList.splice(index, 1);
            // 存
            this.saveTags(this.tagList);
            // 是否是数组最后一项
            let isLastOne = index === this.tagList.length;

            if(isCurrentActive)  {
                // 如果是当前项, 又是最后一项, 那么当前元素为新数组的最后一项; 
                // 如果不是最后一项, index不变
                let newIndex = isLastOne ? this.tagList.length - 1 : index;

                this.setActiveTag(this.tagList[newIndex].name);
                // 页面跳转
                Router.push('/' + this.tagList[newIndex].path);
            }
        },
        // 保存当前tag
        setActiveTag(name) {
            this.activeTag = name;
            sessionStorage.setItem("NJX_ACTIVE_TAG", name);
        },
        // 设置tagList 
        setTags(item) {
            // 列表里有无该TAG: 如果有, 直接设置activeTag即可, 如果没, 将新点击的tag送入tagList中
            let tagList = [...this.tagList];
            let tagIndex = tagList.findIndex((tag) => tag.name === item.name);

            if (tagIndex !== -1) {
                this.activeTag = tagList[tagIndex].name;
            } else {
                tagList.push(item);
                this.saveTags(tagList);
                this.activeTag = tagList[tagList.length - 1].name;
            }
            // 设置当前tag
            this.setActiveTag(this.activeTag);
        },
    },
})

tag页面 (自己乱玩的, 我把tag抽出来了, 所以在使用时需要先设置容器) (代码中的this.$Store是将刚刚的假仓库写在main.js中, 方便全局使用)
代码 ⬇️

<template>
    <div class="tags-wrap" ref="tagWrap" @scroll="contextMenuShow = false">
        <div ref="tagsContainer" class="tags" @click="handleAutoScroll">
            <div
                :class="['tag-item', activeTag === tag.name ? 'active' : '']"
                :style="{
                    marginLeft: tagSeam + 'px',
                    marginRight:
                        index === tagList.length - 1 ? tagSeam + 'px' : '0',
                }"
                v-for="(tag, index) in tagList"
                :key="tag.name"
                @contextmenu.prevent="(event) => showDrop(event, tag)"
            >
                <span>{{ tag.meta.title }}</span>
                <i
                    class="fa fa-times"
                    @click.stop="$Store.removeTag(index)"
                    v-if="!tag.eternalTag"
                ></i>
            </div>
            <div
                class="context-menu"
                :style="{ left: menuLeft + 'px', top: menuTop + 'px' }"
                v-show="contextMenuShow"
                @click="&quot;&quot;;"
            >
                <div
                    :class="[
                        'menu-item',
                        tagEternal && menu.eternalDisabled ? 'disabled' : '',
                    ]"
                    v-for="menu in menuList"
                    :key="menu.operative"
                    @click.stop="
                        tagEternal && menu.eternalDisabled
                            ? ''
                            : handleMenuItem(menu)
                    "
                >
                    {{ menu.label }}
                </div>
            </div>
        </div>
    </div>
</template>

<script>
export default {
    name: "layout-tags",
    computed: {
        activeTag() {
            return this.$Store.activeTag;
        },
    },
    data() {
        return {
            tagSeam: 12,
            contextMenuShow: false,
            menuLeft: 0,
            menuTop: 0,
            menuList: [
                {
                    label: "刷新",
                    operative: "Refresh",
                },
                {
                    label: "关闭",
                    operative: "CloseSelf",
                    // 针对类似“首页”这样, tags栏默认不可关闭的tag
                    eternalDisabled: true,
                },
                {
                    label: "关闭其它",
                    operative: "CloseOthers",
                },
                {
                    label: "关闭所有",
                    operative: "CloseAll",
                },
            ],

            tagList: this.$Store.tagList,
            tagEternal: false,
            currentTag: null,
        };
    },
    methods: {
        handleAutoScroll(e) {
            const el =
                e.srcElement.tagName === "SPAN"
                    ? e.srcElement.parentNode
                    : e.srcElement;

            const wrap = this.$refs.tagWrap; // 可视区宽度: wrap.clientWidth、左滑距离:  wrap.scrollLeft
            const offsetLeft = el.offsetLeft; // 元素距离容器

            // 如果本身是第一个元素, 那么上一个元素就是我自己
            const prevEl = el.previousElementSibling || el;
            const nextEl = el.nextElementSibling || el;

            let autoScrollRight =
                offsetLeft - wrap.scrollLeft <
                prevEl.offsetWidth + this.tagSeam;
            let autoScrollLeft =
                wrap.offsetWidth +
                    wrap.scrollLeft -
                    el.offsetLeft -
                    el.offsetWidth <
                nextEl.offsetWidth + this.tagSeam;

            if (autoScrollRight) {
                wrap.scrollLeft = prevEl.offsetLeft - this.tagSeam;
            }
            if (autoScrollLeft) {
                // wrap.scrollLeft = 'nextEl.offsetLeft' - 'X'
                // X = (wrap.offsetWidth - nextEl.offsetWidth - this.tagSeam);
                wrap.scrollLeft =
                    nextEl.offsetLeft -
                    (wrap.offsetWidth - nextEl.offsetWidth - this.tagSeam);
            }

            // 跳转、设置active
            // 1. 找到可以匹配的唯一值
            let title = el.getElementsByTagName("span")[0].innerHTML;
            // 2. 匹配title
            let activeIndex = this.tagList.findIndex(
                (item) => item.meta.title === title
            );
            let { fullPath: path } = this.tagList[activeIndex];

            this.$router.push({
                path
            });
        },
        showDrop(e, item) {
            // 储存当前tag信息, 一会儿要用
            this.currentTag = item;
            // 根据鼠标位置对contextMenu进行定位
            this.menuLeft = e.clientX + 10;
            this.menuTop = e.clientY + 10;
            // 该tag在tag栏是不是永久展示不可删除
            this.tagEternal = item.eternalTag;
            this.contextMenuShow = true;
            // 点击其它地方, menu消失
            document.addEventListener("click", this.hiddenDrop);
            // 在页面销毁前将事件监听移除
            this.$once("hook:beforeDestroy", () => {
                document.removeEventListener("click", this.hiddenDrop);
            });
        },
        hiddenDrop() {
            this.contextMenuShow = false;
        },
        // 利用tagList中的operative参数, 将方法进行动态设置
        handleMenuItem(menu) {
            let index = this.$Store.sideBarRoutes.findIndex(
                (item) => item.name === this.currentTag.name
            );
            let path = this.$Store.sideBarRoutes[index].fullPath;

            this["tagFor" + menu.operative]({ menu, index, path });

            this.contextMenuShow = false;
        },
        tagForRefresh({ path }) {
            this.$router.replace({
                path: "/redirect",
                query: {
                    path,
                },
            });
        },
        tagForCloseSelf({ index }) {
            this.$Store.removeTag(index);
        },
        tagForCloseOthers({ path }) {
            this.$router.push({
                path,
            });

            this.removeOtherTags(this.currentTag);
        },
        tagForCloseAll() {
            this.removeOtherTags();
        },

        removeOtherTags(item) {
            let constantTags = this.getEternal(this.$Store.constantRoutes);

            let noPush =
                !item ||
                constantTags.findIndex((tag) => tag.name === item.name) !== -1;

            this.tagList = noPush
                ? [...constantTags]
                : constantTags.concat(item);
            this.$Store.saveTags(this.tagList);
            this.$router.push(
                !item ? constantTags[0].fullPath : this.currentTag.fullPath
            );
        },

        getEternal(list) {
            let constantArr = [];
            let getEternal = (arr) => {
                arr.map((item) => {
                    if (item.eternalTag) {
                        constantArr.push(item);
                    }
                    if (item.children) {
                        getEternal(item.children);
                    }
                });
            };
            getEternal(list);
            return constantArr;
        },
        matchTag(item) {
            let activeTag =
                this.tagList[
                    this.tagList.findIndex((tag) => tag.name === item.name)
                ].name;
            this.$Store.setActiveTag(activeTag);
        },
    },
    watch: {
        $route: {
            deep: true,
            immediate: true,
            handler(value) {
                this.matchTag(value);
            },
        },
    },
};
</script>

<style lang="scss" scoped>
@import "@/assets/css/theme.scss";
// 滚动条
::-webkit-scrollbar {
    height: 4px;
}

::-webkit-scrollbar-thumb {
    border-radius: 4px;
    background: rgba(97, 101, 155, 0.4);
}

.tags-wrap {
    height: 36px;

    overflow-x: scroll;
    overflow-y: hidden;
}
.tags {
    display: inline-block;
    white-space: nowrap;

    height: 36px;
}
.tag-item {
    display: inline-block;
    height: 24px;
    line-height: 24px;

    color: #333;
    font-size: 12px;
    border-radius: 4px;
    background: #fff;
    border: 1px solid #e9eaec;
    padding: 0 10px 0 12px;
    cursor: pointer;
    margin: 6px 0 3px 0;
    transition: all ease-in-out 0.2s;
     // 小closeIcon
    .fa {
        display: inline-block;
        margin-left: 8px;
    }
}

.tag-item:hover {
    background: #f8f8f8;
}
.tag-item.active {
    background: rgba(97, 101, 155, 0.8);
    color: #fff;
    border-color: #8085ac !important;
}
.tag-item-active {
    background: #61649f !important;
    color: #fff !important;
}

.context-menu {
    position: fixed;
    z-index: 9;
    background: #fff;

    box-shadow: 2px 2px 3px 0 rgba(128, 118, 163, 0.2);
    border-radius: 4px;
    padding: 5px 0;

    font-size: 12px;
    user-select: none;
}
.menu-item {
    padding: 4px 16px;
    color: #333;
}
.menu-item:hover {
    background: rgba(128, 118, 163, 0.1);
}
.menu-item.disabled {
    color: #c0c4cc;
    cursor: not-allowed;
}
.menu-item.disabled:hover {
    background: transparent !important;
}
</style>

实不相瞒, close的方法是加在每个元素身上的. 后面才写了每个元素的跳转, 正好那个时候刚刚学了通过父元素获取srcElement所以😂有点乱不过功能都完成啦很有趣嘿嘿

tada~~~一个tag栏就完成啦

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容

  • VUE.js 基本命令v-if || v-for : 条件与循环v-model: 为页面输入框进行数据双向绑定v-...
    楼水流云阅读 620评论 0 0
  • client,page和screen的区别? clientX,clientY是触摸点相对于viewport视口x,...
    change_22fa阅读 1,630评论 1 1
  • 一、概念介绍 Vue.js和React.js分别是目前国内和国外最火的前端框架,框架跟类库/插件不同,框架是一套完...
    刘远舟阅读 1,031评论 0 0
  • 1.Vue 2.0 响应式数据的原理 整体思路是数据劫持 + 观察者模式 对象内部通过 defineReactiv...
    潇洒丨阅读 878评论 0 0
  • 一、xue的生命周期是什么 vue每个组件都是独立的,,每个组件都有一个属于他的生命周期,从一个组件创建、数据初始...
    康娜阅读 805评论 0 0