# Split分隔面板
使用Vue3实现的分隔面板,可以将一片区域,分割为可以拖拽调整宽度或高度的两部分区域。
# Split Props
属性 | 说明 | 类型 | 默认值 |
---|---|---|---|
mode | 类型,可选值为 horizontal 或 vertical | String | horizontal |
min | 左侧或者上侧的最小阈值 | String | Number | 40px |
max | 左侧或者上侧的最小阈值(0表示不设置最大值) | String | Number | 0 |
min2 | 右侧或者下侧的最小阈值 | String | Number | 40px |
modelValue | 面板位置,可以是 0~1 (代表百分比),或具体数值的像素(不支持空字符串),可用 v-model 双向绑定;0~1代表百分比,字符串代表像素值 | String |Number | 0.5 |
# Split event
事件名 | 说明 | 参数 |
---|---|---|
on-move-start | 拖拽开始 | event |
on-moving | 拖拽中 | event |
on-move-end | 拖拽结束 | event |
# Split slot
名称 | 说明 |
---|---|
left | mode 为 horizontal 时可用,左边面板 |
right | mode 为 horizontal 时可用,右边面板 |
top | mode 为 vertical 时可用,上边面板 |
bottom | mode 为 vertical 时可用,下边面板 |
trigger | 自定义分隔拖拽节点 |
# 实现代码
XSplit.vue
<template>
<div class="x-split-wrapper"
ref="WrapperEl">
<div :class="{'x-split--horizontal': horizontalMode,'x-split--vertical': verticalMode,}">
<div :class="{'x-split-pane': true, 'x-pane--left': horizontalMode, 'x-pane--top': verticalMode}"
ref="leftTopPaneEl"
:style="{
right: horizontalMode ? rightValueStyle : '',
bottom: verticalMode ? rightValueStyle : '',
}">
<slot v-if="horizontalMode"
name="left"></slot>
<slot v-if="mode === 'vertical'"
name="top"></slot>
</div>
<div class="x-split-trigger-wrapper"
ref="triggerWrapperEl"
:style="{
left: horizontalMode ? leftTopValueStyle : '',
top: verticalMode ? leftTopValueStyle : '',
}"
@mousedown="onMouoseDown($event)">
<div :class="{'x-split-trigger': true, 'x-split-trigger--vertical': horizontalMode, 'x-split-trigger--horizontal': verticalMode}">
<slot name="trigger">
<div v-for="i in 8"
:key="i"
class="x-split-trigger__bar"></div>
</slot>
</div>
</div>
<div :class="{'x-split-pane': true, 'x-pane--right': horizontalMode, 'x-pane--bottom': verticalMode}"
ref="rightBottomPaneEl"
:style="{
left: horizontalMode ? leftTopValueStyle : '',
top: verticalMode ? leftTopValueStyle : '',
}">
<slot v-if="horizontalMode"
name="right"></slot>
<slot v-if="verticalMode"
name="bottom"></slot>
</div>
</div>
</div>
</template>
<script setup>
import { computed, onMounted, onUnmounted, ref, watchEffect } from 'vue';
defineOptions({ name: 'XSplit' });
const props = defineProps({
mode: {
type: String,
default: 'horizontal',
validator: (val) => ['horizontal', 'vertical'].includes(val)
},
modelValue: {
type: [Number, String],
default: 0.5
},
min: {
type: [Number, String],
default: 40
},
max: {
type: [Number, String],
default: 0
},
min2: {
type: [Number, String],
default: 46
}
});
const emits = defineEmits(['update:modelValue', 'on-move-start', 'on-moving', 'on-move-end']);
// emits: ['update:modelValue', 'change'],
const horizontalMode = computed(() => {
return props.mode === 'horizontal';
});
const verticalMode = computed(() => {
return props.mode === 'vertical';
});
const isPercent = ref(false);
let _min = 40;
watchEffect(() => {
try {
_min = parseFloat(props.min);
} catch (error) {
console.error(error);
}
});
let _min2 = 46;
watchEffect(() => {
try {
_min2 = parseFloat(props.min2);
} catch (error) {
console.error(error);
}
});
let _max = 0;
watchEffect(() => {
try {
_max = parseFloat(props.max);
} catch (error) {
console.error(error);
}
});
let _modelValue = parseFloat(props.modelValue);
if (0 <= _modelValue && _modelValue <= 1) {
// 表示百分比
isPercent.value = true;
} else {
// 当做数值型
isPercent.value = false;
}
const leftTopValue = ref(0);
leftTopValue.value = _modelValue;
const WrapperEl = ref(null);
const leftTopPaneEl = ref(null);
const triggerWrapperEl = ref(null);
const rightBottomPaneEl = ref(null);
const leftTopValueStyle = computed(() => {
if (isPercent.value) {
return leftTopValue.value * 100 + '%';
}
return leftTopValue.value + 'px';
});
const rightValueStyle = ref('');
const setRightValueStyle = () => {
if (horizontalMode.value) {
if (isPercent.value) {
rightValueStyle.value = (1 - leftTopValue.value) * 100 + '%';
return;
}
if (WrapperEl.value) {
rightValueStyle.value = WrapperEl.value.offsetWidth - leftTopValue.value + 'px';
}
}
if (verticalMode.value) {
if (isPercent.value) {
rightValueStyle.value = (1 - leftTopValue.value) * 100 + '%';
return;
}
if (WrapperEl.value) {
rightValueStyle.value = WrapperEl.value.offsetHeight - leftTopValue.value + 'px';
}
}
};
let isMoving = false;
let startClientX = 0;
let startLeftWidth = 0;
let startLeftValue = 0;
let startClientY = 0;
let startTopHeight = 0;
let startTopValue = 0;
const onMouoseDown = (event) => {
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
startLeftWidth = leftTopPaneEl.value.offsetWidth;
startTopHeight = leftTopPaneEl.value.offsetHeight;
startLeftValue = leftTopValue.value;
startTopValue = leftTopValue.value;
startClientX = event.clientX;
startClientY = event.clientY;
isMoving = true;
emits('on-move-start', event);
};
const execHorizontal = (event) => {
if (isPercent.value) {
const deltaWidth = event.clientX - startClientX;
const wrapperWidth = WrapperEl.value.offsetWidth;
let _leftTopValue = (startLeftWidth + deltaWidth) / wrapperWidth;
if (_leftTopValue <= 0) {
_leftTopValue = 0;
}
if (_leftTopValue >= 1) {
_leftTopValue = 1;
}
_leftTopValue = _leftTopValue.toFixed(6) - 0;
const leftWidth = wrapperWidth * _leftTopValue;
if (leftWidth <= _min) {
_leftTopValue = (_min / wrapperWidth).toFixed(6) - 0;
}
if (_max > 0 && leftWidth >= _max) {
_leftTopValue = (_max / wrapperWidth).toFixed(6) - 0;
}
if (wrapperWidth * (1 - _leftTopValue) <= _min2) {
_leftTopValue = (wrapperWidth - _min2) / wrapperWidth;
}
leftTopValue.value = _leftTopValue;
setRightValueStyle();
emits('update:modelValue', leftTopValue.value);
} else {
const deltaWidth = event.clientX - startClientX;
let _leftTopValue = startLeftValue + deltaWidth;
const wrapperWidth = WrapperEl.value.offsetWidth;
if (_leftTopValue <= 0) {
_leftTopValue = 0;
}
if (_leftTopValue >= wrapperWidth) {
_leftTopValue = wrapperWidth;
}
if (_leftTopValue <= _min) {
_leftTopValue = _min;
}
if (_max > 0 && _leftTopValue >= _max) {
_leftTopValue = _max;
}
if (wrapperWidth - _leftTopValue <= _min2) {
_leftTopValue = wrapperWidth - _min2;
}
leftTopValue.value = _leftTopValue;
setRightValueStyle();
emits('update:modelValue', leftTopValue.value + 'px');
}
};
const execVertical = (event) => {
if (isPercent.value) {
const deltaHeight = event.clientY - startClientY;
let _leftTopValue = (startTopHeight + deltaHeight) / WrapperEl.value.offsetHeight;
if (_leftTopValue <= 0) {
_leftTopValue = 0;
}
if (_leftTopValue >= 1) {
_leftTopValue = 1;
}
const wrapperHeight = WrapperEl.value.offsetHeight;
_leftTopValue = _leftTopValue.toFixed(6) - 0;
const topHeight = wrapperHeight * _leftTopValue;
if (topHeight <= _min) {
_leftTopValue = (_min / wrapperHeight).toFixed(6) - 0;
}
if (_max > 0 && topHeight >= _max) {
_leftTopValue = (_max / wrapperHeight).toFixed(6) - 0;
}
if (wrapperHeight * (1 - _leftTopValue) <= _min2) {
_leftTopValue = (WrapperEl.value.offsetHeight - _min2) / WrapperEl.value.offsetHeight;
}
leftTopValue.value = _leftTopValue;
setRightValueStyle();
emits('update:modelValue', leftTopValue.value);
} else {
const deltaHeight = event.clientY - startClientY;
let _leftTopValue = startTopValue + deltaHeight;
const wrapperHeight = WrapperEl.value.offsetHeight;
if (_leftTopValue <= 0) {
_leftTopValue = 0;
}
if (_leftTopValue >= wrapperHeight) {
_leftTopValue = wrapperHeight;
}
if (_leftTopValue <= _min) {
_leftTopValue = _min;
}
if (_max > 0 && _leftTopValue >= _max) {
_leftTopValue = _max;
}
if (wrapperHeight - _leftTopValue <= _min2) {
_leftTopValue = wrapperHeight - _min2;
}
leftTopValue.value = _leftTopValue;
setRightValueStyle();
emits('update:modelValue', leftTopValue.value + 'px');
}
};
const onMouseMove = (event) => {
if (isMoving) {
if (horizontalMode.value) {
execHorizontal(event);
}
if (verticalMode.value) {
execVertical(event);
}
emits('on-moving', event);
}
};
const onMouseUp = (event) => {
if (isMoving) {
if (horizontalMode.value) {
execHorizontal(event);
}
if (verticalMode.value) {
execVertical(event);
}
}
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
isMoving = false;
startClientX = 0;
startLeftWidth = 0;
emits('on-move-end', event);
};
onMounted(() => {
setRightValueStyle();
});
onUnmounted(() => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
});
</script>
<style scoped lang="css">
.x-split-wrapper {
width: 100%;
height: 100%;
position: relative;
}
.x-split-pane {
position: absolute;
z-index: 1;
}
.x-split-pane.x-pane--left {
left: 0;
top: 0;
bottom: 0;
}
.x-split-pane.x-pane--top {
left: 0;
right: 0;
top: 0;
}
.x-split-pane.x-pane--right {
right: 0;
top: 0;
bottom: 0;
padding-left: 6px;
}
.x-split-pane.x-pane--bottom {
right: 0;
left: 0;
bottom: 0;
padding-top: 6px;
}
.x-split-trigger-wrapper {
position: absolute;
/* transform: translate(-50%, -50%); */
z-index: 10;
}
.x-split--horizontal > .x-split-trigger-wrapper {
height: 100%;
width: 0;
}
.x-split--vertical > .x-split-trigger-wrapper {
height: 0;
width: 100%;
}
.x-split-trigger {
border: 1px solid #d7dde4;
}
.x-split-trigger--vertical {
width: 6px;
box-sizing: border-box;
height: 100%;
background: #f8f8f9;
border-top: none;
border-bottom: none;
cursor: col-resize;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.x-split-trigger--vertical .x-split-trigger__bar {
width: 4px;
height: 1px;
margin: 2px;
background-color: rgba(23, 35, 61, 0.25);
}
.x-split-trigger--horizontal {
height: 6px;
box-sizing: border-box;
width: 100%;
background: #f8f8f9;
border-left: none;
border-right: none;
cursor: row-resize;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.x-split-trigger--horizontal .x-split-trigger__bar {
width: 1px;
height: 4px;
margin: 0 2px;
background-color: rgba(23, 35, 61, 0.25);
}
</style>
# 基础用法
<XSplit v-model="splitValue">
<template v-slot:left>
<div>top box</div>
</template>
<template v-slot:right>
<div>bottom box</div>
</template>
</XSplit>
# 嵌套使用
<div style="margin: 20px;width: 500px; height: 300px;border: 1px solid #eee;">
<XSplit :modelValue="200">
<template v-slot:left>
<div style="height: 100%">
<XSplit :modelValue="100"
mode="vertical"
@on-moving="onMoving">
<template v-slot:top>
<div>top box</div>
</template>
<template v-slot:bottom>
<div>bottom box</div>
</template>
</XSplit>
</div>
</template>
<template v-slot:right>
<div>right box</div>
</template>
</XSplit>
</div>