【sgSpliter】自定义组件:可调整宽度、高度、折叠的分割线
特性:
- 允许设置显示折叠按钮
- 允许设置折叠线按钮位置
- 允许设置当拖拽区域到0,再点击箭头展开的默认宽度
- 允许设置当拖拽宽度小于此宽度,自动折叠到0
- 允许设置指定最小宽度
- 允许设置指定最大宽度
- 允许设置按钮风格:白色背景default、蓝色背景blue
- 允许设置分隔条大小,默认 2px
- 允许设置指定 是否可调整大小,会影响相邻
- 允许设置外部触发显示折叠按钮
- 允许设置禁止双击折叠线
sgSpliter.vue
<template>
<!-- 注意:
父组件position必须是relative、absolute或fixed,
不建议直接在绑定:data后面用"{属性}",
建议单独在script中声明data,避免拖拽过程重复调用
-->
<div
:class="$options.name"
:placement="placement"
@mousedown="__addWindowEvents"
@dblclick="dblclickSpliterLine"
:unresizable="!resizable"
:mouseoverShowArrowBtn="mouseoverShowArrowBtn"
>
<div
v-if="showArrowBtn"
class="arrow-btn"
@click="clickArrowBtn"
@mousedown.stop
:styleType="arrowBtnStyleType"
:collapse="collapse"
>
<!-- 箭头在父组件的最左侧 -->
<template v-if="placement === `left`">
<i class="el-icon-arrow-left" v-if="collapse" />
<i class="el-icon-arrow-right" v-else />
</template>
<!-- 箭头在父组件的最右侧 -->
<template v-if="placement === `right`">
<i class="el-icon-arrow-right" v-if="collapse" />
<i class="el-icon-arrow-left" v-else />
</template>
<!-- 箭头在父组件的最上侧 -->
<template v-if="placement === `top`">
<i class="el-icon-arrow-up" v-if="collapse" />
<i class="el-icon-arrow-down" v-else />
</template>
<!-- 箭头在父组件的最下侧 -->
<template v-if="placement === `bottom`">
<i class="el-icon-arrow-down" v-if="collapse" />
<i class="el-icon-arrow-up" v-else />
</template>
</div>
</div>
</template>
<script>
export default {
name: "sgSpliter",
components: {},
data() {
return {
form: {},
collapse: false,
showArrowBtn: true,//显示折叠按钮
placement: `right`,//折叠线按钮位置
parent: null,
defaultSize: 200, //当拖拽区域到0,再点击箭头展开的默认宽度
nearEdgeSize: 5, //当拖拽宽度小于此宽度,自动折叠到0
minSize: null, //可选,指定 最小宽度
maxSize: null, //可选,指定 最大宽度
size: null,
size_bk: null,
arrowBtnStyleType: `default`, //按钮风格:白色背景default、蓝色背景blue
splitBarSize: 1, //可选,分隔条大小,默认 2px
resizable: true, //可选,指定 是否可调整大小,会影响相邻
mouseoverShowArrowBtn: false, //外部触发显示折叠按钮
disabledDblclickSpliterLine: false, //禁止双击折叠线
};
},
props: ["data"],
computed: {},
watch: {
data: {
handler(newValue, oldValue) {
// console.log(`深度监听${this.$options.name}:`, newValue, oldValue);
if (Object.keys(newValue || {}).length) {
this.form = JSON.parse(JSON.stringify(newValue));
this.$g.convertForm2ComponentParam(`showArrowBtn`, this);
this.$g.convertForm2ComponentParam(`defaultSize`, this);
this.$g.convertForm2ComponentParam(`nearEdgeSize`, this);
this.$g.convertForm2ComponentParam(`minSize`, this);
this.$g.convertForm2ComponentParam(`maxSize`, this);
this.$g.convertForm2ComponentParam(`placement`, this);
this.$g.convertForm2ComponentParam(`parent`, this);
this.$g.convertForm2ComponentParam(`arrowBtnStyleType`, this);
this.$g.convertForm2ComponentParam(`splitBarSize`, this);
this.$g.convertForm2ComponentParam(`resizable`, this);
this.$g.convertForm2ComponentParam(`mouseoverShowArrowBtn`, this);
this.$g.convertForm2ComponentParam(`disabledDblclickSpliterLine`, this);
this.form.hasOwnProperty("collapse") &&
this.collapseSpliter({ collapse: this.form.collapse }); //允许外部控制默认折叠或展开
this.$nextTick(() => {
this.$el.style.setProperty(`--splitBarSize`, `${this.splitBarSize}px`); //js往css传递局部参数
});
// 不要在这里初始化size_bk,由于拖拽过程会重复触发这里的代码执行
}
},
deep: true, //深度监听
immediate: true, //立即执行
},
size(size) {
size <= this.nearEdgeSize && (size = 0);
this.collapse = size === 0;
this.$emit(`sizeChange`, { size });
},
collapse(collapse) {
this.$emit(`collapseChange`, { collapse });
},
},
mounted() {
this.$nextTick(() => {
this.parent || (this.parent = this.$el.parentNode);
if (this.parent) {
let rect = this.parent.getBoundingClientRect();
switch (this.placement) {
case `left`: // 竖线在父组件的最左侧
case `right`: // 竖线在父组件的最右侧
this.size_bk = rect.width;
break;
case `top`: // 竖线在父组件的最上侧
case `bottom`: // 竖线在父组件的最下侧
this.size_bk = rect.height;
break;
default:
}
}
});
},
beforeDestroy() {
this.__removeWindowEvents();
},
methods: {
//size发生变化的时候就做缓动效果
changeTransitionSize() {
let parent = this.parent;
if (parent) {
let attr = `${this.$options.name}-transitionSize`;
parent.setAttribute(attr, true);
setTimeout(() => parent.removeAttribute(attr), 200);
}
},
clickArrowBtn($event) {
this.collapseSpliter();
},
dblclickSpliterLine($event) {
if (this.disabledDblclickSpliterLine || !this.resizable) return;
this.collapseSpliter();
},
collapseSpliter({ collapse } = {}) {
this.collapse = collapse === undefined ? !this.collapse : collapse;
let expandSize = this.size_bk > this.nearEdgeSize ? this.size_bk : this.defaultSize;
this.changeTransitionSize();
this.size = this.collapse ? 0 : expandSize;
this.$emit(`sizeChange`, { size: this.size }); //避免上一次size=0的时候,第二次不监听变化
},
bkSize(d) {
this.size_bk = this.size;
},
__addWindowEvents() {
this.__removeWindowEvents();
addEventListener("mousemove", this.mousemove_window);
addEventListener("mouseup", this.mouseup_window);
},
__removeWindowEvents() {
removeEventListener("mousemove", this.mousemove_window);
removeEventListener("mouseup", this.mouseup_window);
},
mousemove_window($event) {
if (!this.resizable) return;
this.parent || (this.parent = this.$el.parentNode);
if (this.parent) {
let { x, y } = $event,
rect = this.parent.getBoundingClientRect(),
size;
switch (this.placement) {
case `left`: // 竖线在父组件的最左侧
size = rect.x + rect.width - x;
break;
case `right`: // 竖线在父组件的最右侧
size = x - rect.x;
break;
case `top`: // 竖线在父组件的最上侧
size = rect.y + rect.height - y;
break;
case `bottom`: // 竖线在父组件的最下侧
size = y - rect.y;
break;
default:
}
this.minSize && size < this.minSize && (size = this.minSize);
this.maxSize && size > this.maxSize && (size = this.maxSize);
this.size = size;
this.bkSize();
} else {
this.$message.error(`没有获取到父组件parent!`);
}
},
mouseup_window($event) {
this.__removeWindowEvents();
},
},
};
</script>
<style lang="scss" scoped>
$splitBarSize: var(--splitBarSize); //css获取js传递的参数
.sgSpliter {
z-index: 1;
background-color: #efefef;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
.arrow-btn {
transition: 0.382s;
opacity: 0;
pointer-events: none;
// transform: translateY(50%); //防止托盘最小高度的时候还冒出一小截
width: 20px;
height: 20px;
display: flex;
justify-content: center;
align-items: center;
color: #409eff;
background-color: white;
font-size: 12px;
position: absolute;
margin: auto;
box-sizing: border-box;
cursor: pointer;
&:hover {
filter: brightness(1.1);
}
&[styleType="blue"] {
color: white;
background-color: #4f6bdf;
}
&[collapse] {
opacity: 1;
pointer-events: auto;
}
}
// 位置----------------------------------------
&[placement="left"],
&[placement="right"] {
cursor: col-resize;
width: $splitBarSize;
height: 100%;
}
&[placement="top"],
&[placement="bottom"] {
cursor: row-resize;
width: 100%;
height: $splitBarSize;
}
&[placement="left"] {
left: 0;
right: revert;
.arrow-btn {
left: revert;
right: $splitBarSize;
top: 0;
bottom: 0;
border-radius: 8px 0 0 8px;
padding: 20px 0;
box-shadow: -5px 0px 10px 0 rgba(0, 0, 0, 0.1);
}
}
&[placement="right"] {
left: revert;
right: 0;
.arrow-btn {
left: $splitBarSize;
right: revert;
top: 0;
bottom: 0;
border-radius: 0 8px 8px 0;
padding: 20px 0;
box-shadow: 5px 0px 10px 0 rgba(0, 0, 0, 0.1);
}
}
&[placement="top"] {
top: 0;
bottom: revert;
.arrow-btn {
left: 0;
right: 0;
top: revert;
bottom: $splitBarSize;
border-radius: 8px 8px 0 0;
padding: 0 20px;
box-shadow: 0px -5px 10px 0 rgba(0, 0, 0, 0.1);
}
}
&[placement="bottom"] {
top: revert;
bottom: 0;
.arrow-btn {
left: 0;
right: 0;
top: $splitBarSize;
bottom: revert;
border-radius: 0 0 8px 8px;
padding: 0 20px;
box-shadow: 0px 5px 10px 0 rgba(0, 0, 0, 0.1);
}
}
// ----------------------------------------
&[mouseoverShowArrowBtn],
&:hover {
background-color: #b3d8ff;
.arrow-btn {
opacity: 1;
pointer-events: auto;
}
}
// 按下拖拽线条后出现的半透明区域
&::after {
content: "";
transition: 0.382s;
position: absolute;
background-color: #409eff22;
opacity: 0;
}
$splitOpacityBgExpandSize: 5px; //半透明延伸宽度
$splitOpacityBgSize: calc(#{$splitOpacityBgExpandSize} * 2 + #{$splitBarSize});
&[placement="left"],
&[placement="right"] {
&::after {
width: $splitOpacityBgSize;
height: 100%;
left: -#{$splitOpacityBgExpandSize};
top: 0;
}
}
&[placement="top"],
&[placement="bottom"] {
&::after {
width: 100%;
height: $splitOpacityBgSize;
left: 0;
top: -#{$splitOpacityBgExpandSize};
}
}
&:active {
opacity: 1;
background-color: #409eff;
&::after {
opacity: 1;
}
}
// 禁止拖拽
&[unresizable] {
cursor: default;
background-color: transparent;
&::after {
content: none;
}
}
}
</style>
<style lang="scss">
[sgSpliter-transitionSize] {
transition: 0.2s;
}
</style>
demo
<template>
<div :class="$options.name">
<div class="left" :style="{ width: `${leftWidth}px` }">
<sgSpliter :data="{ placement: `right` }" @sizeChange="leftWidth = $event.size" />
</div>
<div class="right">
<div class="top" :style="{ height: `${topHeight}px` }">
<sgSpliter
:data="{ placement: `bottom` }"
@sizeChange="topHeight = $event.size"
/>
</div>
<div class="bottom">
<div class="left">
<div class="top"></div>
<div class="bottom" :style="{ height: `${bottomHeight}px` }">
<sgSpliter
:data="{ placement: `top` }"
@sizeChange="bottomHeight = $event.size"
/>
</div>
</div>
<div class="right" :style="{ width: `${bottomWidth}px` }">
<sgSpliter
:data="{ placement: `left` }"
@sizeChange="bottomWidth = $event.size"
/>
</div>
</div>
</div>
</div>
</template>
<script>
import sgSpliter from "@/vue/components/admin/sgSpliter";
export default {
name: `demoSpliter`,
components: { sgSpliter },
data() {
return {
leftWidth: 200,
topHeight: 200,
bottomHeight: 200,
bottomWidth: 200,
};
},
};
</script>
<style lang="scss" scoped>
.demoSpliter {
display: flex;
& > .left {
height: 100%;
flex-shrink: 0;
position: relative;
box-sizing: border-box;
border-right: 1px solid #eee;
}
& > .right {
flex-grow: 1;
display: flex;
flex-direction: column;
& > .top {
flex-shrink: 0;
width: 100%;
position: relative;
box-sizing: border-box;
border-bottom: 1px solid #eee;
}
& > .bottom {
flex-grow: 1;
width: 100%;
display: flex;
& > .left {
flex-grow: 1;
height: 100%;
display: flex;
flex-direction: column;
& > .top {
flex-grow: 1;
width: 100%;
}
& > .bottom {
flex-shrink: 0;
width: 100%;
position: relative;
box-sizing: border-box;
border-top: 1px solid #eee;
}
}
& > .right {
flex-shrink: 0;
height: 100%;
position: relative;
box-sizing: border-box;
border-left: 1px solid #eee;
}
}
}
}
</style>