效果图
抱歉嘿嘿
文里的tagList和activeTag全部存sessionStorage里了, 现在想来没那个必要😂存那里和放假仓库里是一回事, 不刷新都在, 刷新就没......已然记不起当时为什么要这么地了...........这里的代码不想改了, 真有用到的那么地倒是也无大碍哈哈哈哈哈哈哈
功能:
- 点击跳转对应页面;
- 设置activeTag;
- 样式方面: 如果点击左数第二个时, 左数第一个没有完全露出, 让容器滚动使前一个tag露出; 右边同样(之前没有关注过, 写到这里才发现, 没有scrollRight和offsetRight啊😂);
- 右键小菜单: 位置跟着鼠标走(功能如图);
- 设置固定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=""";"
>
<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栏就完成啦