一个后台管理常常需要一个标签页来管理已经打开的页面,这里我们单独写一个组件来展示标签页数组。
该标签页组件只做展示不涉及操作数据。标签页数组可记录已打开的数组,还能定义什么页面需要缓存,是一个重要的功能呢。
首先,建立一个TagList.vue组件,里面代码如下
<template>
<div
class="tag-list-cp-container"
ref="TagListRef">
<div
class="left"
@wheel="handleScroll">
<el-scrollbar
ref="ElScrollbarRef"
height="100%">
<draggable
class="scrollbar-container"
item-key="sign"
v-model="tagListTrans">
<template #item="{element}">
<div
:class="{
'item':true,
'active':dataContainer.activeSign==element.sign,
}"
@click="handleClick(element)"
@contextmenu.prevent="e=>{
handleClickContext(e,element);
}">
<SvgIcon
class="sign icon-sign"
v-if="element.showTagIcon && element.iconName"
:style="'width: 15px;min-width:15px;height: 15px;'"
:name="element.iconName"></SvgIcon>
<div
class="sign"
v-else-if="dataContainer.activeSign==element.sign">
</div>
{{element.title}}
<div
v-if="!element.fixed"
@click.stop="handleRemove(element)"
class="bt">
<SvgIcon
:style="'width:12px;height:12px;'"
name="times"></SvgIcon>
</div>
<div
v-if="element.isCache"
class="cache"></div>
</div>
</template>
</draggable>
</el-scrollbar>
</div>
<div class="bt-list">
<div
class="bt"
@click="handleOptionClick(5)">
<SvgIcon
:style="'width:15px;height:15px;'"
name="redo"></SvgIcon>
</div>
<div
class="bt"
@click="handleToLeft()">
<SvgIcon
:style="'width:15px;height:15px;'"
name="arrow-left"></SvgIcon>
</div>
<div
class="bt"
@click="handleToRight()">
<SvgIcon
:style="'width:15px;height:15px;'"
name="arrow-right"></SvgIcon>
</div>
</div>
<div
ref="RightOptionRef"
class="right">
<div
@click="()=>{
dataContainer.show_1 = !dataContainer.show_1;
}"
class="bt">
<SvgIcon
:style="'width:20px;height:20px;'"
name="icon-drag"></SvgIcon>
</div>
<div
v-if="dataContainer.show_1"
class="bt-list-container">
<div
v-if="dataContainer.tagList.length>1"
class="item"
@click="handleOptionClick(1)">
<SvgIcon
:style="'width:16px;height:16px;color:#f86464;'"
name="times"></SvgIcon>
关闭当前标签页
</div>
<div
v-if="dataContainer.tagList.length>1"
class="item"
@click="handleOptionClick(2)">
<SvgIcon
:style="'width:16px;height:16px;color:#f86464;'"
name="borderverticle-fill"></SvgIcon>
关闭其他标签页
</div>
<div
v-if="dataContainer.tagList.length>1"
class="item"
@click="handleOptionClick(3)">
<SvgIcon
:style="'width:16px;height:16px;color:#f86464;'"
name="arrow-left"></SvgIcon>
关闭左边标签页
</div>
<div
v-if="dataContainer.tagList.length>1"
class="item"
@click="handleOptionClick(4)">
<SvgIcon
:style="'width:16px;height:16px;color:#f86464;'"
name="arrow-right"></SvgIcon>
关闭右边标签页
</div>
<div
class="item re-bt"
@click="handleOptionClick(5)">
<SvgIcon
:style="'width:16px;height:16px;color:#0072E5;'"
name="redo"></SvgIcon>
刷新当前标签页
</div>
<div
class="item"
@click="handleOptionClick(6)">
<SvgIcon
:style="'width:16px;height:16px;color:#0072E5;'"
name="expand-alt"></SvgIcon>
视图全屏(Esc键退出)
</div>
</div>
</div>
<div
v-if="dataContainer.show"
:style="{
'--location-x':`${dataContainer.location.x || 0}px`,
'--location-y':`${dataContainer.location.y || 0}px`,
}"
class="bt-list-container">
<div
class="item"
@click="handleSwitchCache()">
<SvgIcon
:style="'width:16px;height:16px;'"
name="switch"></SvgIcon>
切换缓存状态
</div>
<div
class="item"
@click="handleSwitchFixed()">
<SvgIcon
:style="'width:16px;height:16px;'"
name="nail"></SvgIcon>
切换固定状态
</div>
<div
class="item re-bt"
@click="handleRefresh()">
<SvgIcon
:style="'width:16px;height:16px;color:#0072E5;'"
name="redo"></SvgIcon>
刷新此标签页
</div>
<div
class="item"
@click="handleOptionClick(6)">
<SvgIcon
:style="'width:16px;height:16px;color:#0072E5;'"
name="expand-alt"></SvgIcon>
视图全屏
</div>
</div>
</div>
</template>
<script>
/*
* 标签切换按钮组件
* 由外部指定数据
*/
import {
defineComponent,ref,reactive,
computed,onMounted,watch,toRef,
onUnmounted,
nextTick,
} from "vue";
import SvgIcon from "@/components/svgIcon/index.vue";
import draggable from 'vuedraggable';
export default {
name: 'TagList',
components: {
SvgIcon,
draggable,
},
props:{
/**
* 所显示的标签列表
* */
/**
* 一个tag例子的属性介绍
*/
// {
// title:'标签一', //标签标题
// sign:'/main/index', //唯一标识
// fullPath:'/main/index', //跳转地址,完整地址
// isCache:true, //该标签页面是否缓存
// fixed:false, //是否固定,不可删除
// }
tagList:{
type:Array,
default:()=>{
return [];
},
},
/** 当前活动的唯一标识 */
activeSign:{
type:[Number,String],
default:0,
},
},
emits:[
'onChange','onClick','onRemove','onOptionClick','onSwitchCache','onSwitchFixed',
'onRefresh',
],
setup(props,{emit}){
const ElScrollbarRef = ref(null);
const TagListRef = ref(null);
const RightOptionRef = ref(null);
const dataContainer = reactive({
tagList:toRef(props,'tagList'),
activeSign:toRef(props,'activeSign'),
show:false,
location:{},
show_1:false,
});
const otherDataContainer = {
activeItem:null,
};
/** 用来排序转换的数组,由外部确定是否转换 */
const tagListTrans = computed({
get(){
return dataContainer.tagList;
},
set(value){
emit('onChange',value);
},
});
/** 标签点击事件,向外部抛出 */
function handleClick(item){
emit('onClick',item);
}
/** 标签删除事件 */
function handleRemove(item){
emit('onRemove',item);
}
/** 操作事件 */
function handleOptionClick(type){
emit('onOptionClick',type);
}
/**
* 鼠标滚动事件
* 横向滚动标签页
* */
function handleScroll(e){
if(!ElScrollbarRef.value) return;
/** shift + 鼠标滚轮可以横向滚动 */
if(e.shiftKey) return;
let el = ElScrollbarRef.value.wrapRef;
let scrollLeft = el.scrollLeft;
if(e.deltaY < 0){
scrollLeft = scrollLeft - 30;
}else{
scrollLeft = scrollLeft + 30;
}
el.scrollLeft = scrollLeft;
}
/**
* 自动滚动到相应标签
* 防止标签没在视区
*/
function autoScroll(){
nextTick(()=>{
if(!ElScrollbarRef.value) return;
let el = ElScrollbarRef.value.wrapRef;
let target = el.querySelector('.item.active');
if(!target) return;
let rect = el.getBoundingClientRect();
let rect_1 = target.getBoundingClientRect();
if(rect_1.x < rect.x){
// 表示在左边遮挡
let scroll = rect.x - rect_1.x;
el.scrollLeft = el.scrollLeft - scroll - 5;
}
if((rect_1.x + rect_1.width) > (rect.x + rect.width)){
// 表示在右边遮挡
let scroll = rect_1.x - (rect.x + rect.width);
el.scrollLeft = el.scrollLeft + scroll + rect_1.width + 5;
}
});
}
watch(toRef(props,'activeSign'),()=>{
autoScroll();
});
onMounted(()=>{
autoScroll();
});
/** 鼠标右击,展示自定义右击面板 */
function handleClickContext(e,item){
if(!TagListRef.value) return;
let el = TagListRef.value;
let el_1 = e.target;
let rect = el.getBoundingClientRect();
let rect_1 = el_1.getBoundingClientRect();
let location = {
x:rect_1.x - rect.x,
y:rect_1.y - rect.y + rect_1.height,
};
dataContainer.location = location;
dataContainer.show = true;
otherDataContainer.activeItem = item;
}
/** 初始化隐藏事件 */
function initHiddenEvent(){
function callbackFn(e){
dataContainer.show = false;
}
document.addEventListener('click', callbackFn);
onUnmounted(()=>{
document.removeEventListener('click', callbackFn);
});
}
initHiddenEvent();
/**
* 切换缓存状态
* 由外部实现
* */
function handleSwitchCache(){
if(!otherDataContainer.activeItem) return;
emit('onSwitchCache',otherDataContainer.activeItem);
}
/**
* 切换固定状态
* 由外部实现
* */
function handleSwitchFixed(){
if(!otherDataContainer.activeItem) return;
emit('onSwitchFixed',otherDataContainer.activeItem);
}
/**
* 刷新标签页
* 由外部实现
* */
function handleRefresh(){
if(!otherDataContainer.activeItem) return;
emit('onRefresh',otherDataContainer.activeItem);
}
/** 跳转到右侧 */
function handleToRight(){
let index = dataContainer.tagList.findIndex(item=>{
return item.sign == dataContainer.activeSign;
});
if(index == -1) return;
let target = dataContainer.tagList[index + 1];
if(!target) return;
handleClick(target);
}
/** 跳转到左侧 */
function handleToLeft(){
let index = dataContainer.tagList.findIndex(item=>{
return item.sign == dataContainer.activeSign;
});
if(index == -1) return;
let target = dataContainer.tagList[index - 1];
if(!target) return;
handleClick(target);
}
/** 初始化隐藏事件 */
function initHiddenEvent_1(){
function callbackFn(e){
if(!RightOptionRef.value) return;
if(!e || !e.target) return;
if(RightOptionRef.value.contains(e.target)) return;
dataContainer.show_1 = false;
}
document.addEventListener('click', callbackFn);
onUnmounted(()=>{
document.removeEventListener('click', callbackFn);
});
}
initHiddenEvent_1();
return {
dataContainer,
handleClick,
handleRemove,
handleOptionClick,
tagListTrans,
handleScroll,
ElScrollbarRef,
handleClickContext,
TagListRef,
handleSwitchCache,
handleSwitchFixed,
handleRefresh,
handleToRight,
handleToLeft,
RightOptionRef,
};
},
}
</script>
<style scoped lang="scss">
.tag-list-cp-container {
height: 100%;
width: 100%;
padding: 0;
box-sizing: border-box;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
color: var(--text-color);
>.left{
flex: 1 1 0;
width: 0;
height: 100%;
:deep(.el-scrollbar__bar){
&.is-horizontal{
height: 5px !important;
opacity: 0.5;
}
}
:deep(.el-scrollbar__view){
height: 100%;
}
:deep(.scrollbar-container){
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
width: fit-content;
height: 100%;
.item{
cursor: pointer;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
padding: 5px 8px;
box-sizing: border-box;
margin-left: 5px;
font-size: 13px;
height: 30px;
width: max-content;
border-radius: 3px;
color: #606266;
position: relative;
transition: all 0.2s;
&:last-child{
margin-right: 5px;
}
&.active{
background-color: #5240ff30;
color: #5240ff;
font-weight: bold;
box-shadow: inset 0 1px 4px #00000034;
// border:1px solid rgb(196, 196, 196);
}
&:hover{
background-color: #5240ff30;
color: #5240ff;
}
>.sign{
width: 10px;
height: 10px;
border-radius: 50%;
background-color: #5240ff;
margin-right: 5px;
&.icon-sign{
background-color: transparent;
}
}
>.bt{
width: fit-content;
height: fit-content;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
margin-left: 5px;
}
>.cache{
width: 30%;
max-width: 30px;
min-width: 15px;
height: 3px;
border-radius: 999px;
background-color: #5340ff34;
position: absolute;
bottom: 0;
}
}
}
}
>.bt-list{
display: flex;
flex-direction: row;
align-items: center;
padding: 0 10px;
box-sizing: border-box;
border-left: 1px solid var(--border-color);
box-shadow: inset 0 1px 4px #00000010;
height: 100%;
>*{
margin: 0 10px 0 0;
&:last-child{
margin: 0;
}
}
>.bt{
cursor: pointer;
transition: all 0.2s;
height: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
&:hover{
color: #5240ff;
}
}
}
>.right{
width: 40px;
height: 100%;
border-left: 1px solid var(--border-color);
box-sizing: border-box;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
position: relative;
box-shadow: inset 0 1px 4px #00000010;
>.bt{
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
cursor: pointer;
transition: all 0.2s;
&:hover{
color: #5240ff;
}
}
>.bt-list-container{
width: max-content;
min-width: 150px;
position: absolute;
z-index: 9;
top: calc(100% + 0px);
right: 5px;
background-color: rgb(255, 255, 255);
box-shadow: 0 3px 8px 0 rgba(0, 0, 0, 0.5);
padding: 10px 0;
box-sizing: border-box;
border-radius: 2px;
overflow: hidden;
transition: opacity 0.2s;
font-size: 15px;
>.item{
cursor: pointer;
width: auto;
min-width: max-content;
transition: all 0.2s;
padding: 13px 15px;
box-sizing: border-box;
display: block;
color: #6b7386;
text-align: left;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
>*{
margin-right: 10px;
}
&:hover{
box-shadow: inset 0 1px 4px #0000001f;
background-color: #fef0f0;
color: #f56c6c;
}
&.re-bt{
background-color: rgba(194, 224, 255, 0.5);
color: #0072E5;
&:hover{
background-color: rgba(194, 224, 255, 0.5);
color: #0072E5;
}
}
}
}
}
>.bt-list-container{
width: max-content;
min-width: 150px;
position: absolute;
z-index: 9;
top: var(--location-y);
left: var(--location-x);
background-color: rgb(255, 255, 255);
box-shadow: 0 3px 8px 0 rgba(0, 0, 0, 0.5);
padding: 10px 0;
box-sizing: border-box;
border-radius: 2px;
overflow: hidden;
opacity: 1;
transition: opacity 0.2s;
font-size: 15px;
>.item{
cursor: pointer;
width: auto;
min-width: max-content;
transition: all 0.2s;
padding: 13px 15px;
box-sizing: border-box;
display: block;
color: #6b7386;
text-align: left;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
>*{
margin-right: 10px;
}
&:hover{
box-shadow: inset 0 1px 4px #0000001f;
background-color: #fef0f0;
color: #f56c6c;
}
&.re-bt{
background-color: rgba(194, 224, 255, 0.5);
color: #0072E5;
&:hover{
background-color: rgba(194, 224, 255, 0.5);
color: #0072E5;
}
}
}
}
}
</style>
这里我们使用了el-scrollbar组件来管理滚动容器,SvgIcon来管理icon的展示,vuedraggable来管理拖拽排序。
该组件接受的数据源为 tagList,activeSign。
tagList:标签的数组。
activeSign:当前活动的标签的sign字符串,每个标签是一个对象,对象有sign唯一标识属性。
组件核心思想:该组件使用外部数据源保证组件灵活性,自身集合多种操作但不处理,抛出给外部处理。只做数据的展示。
源码地址
DEMO