前端vue左侧树的一整套功能实现(一):vue2+vite封装v-resize指令,实现左侧树拖拽宽度和折叠展开
实现v-resize指令,具体以下功能:
- 指令接收宽度最大最小值,接收一个id用于localStorage存储拖拽宽度,接收padding
- 拖拽时产生虚线拖拽,松开鼠标再进行元素宽度调整
- 折叠展开图标使用本地图片
封装一个vite下使用本地图片的函数方法
用于拖拽指令中设置折叠展开图标
/** vite使用动态图片的方式 */
export function requireImg(name) {
return new URL(`/src/assets/imgs/${name}`, import.meta.url).href
}
v-resize指令具体代码
// 注意,需要去除绑定元素的overflow:auto,指令会添加一个具有overflow:auto的元素
let resize;
let stopResize;
const vResize = {
bind(el, binding) {
// 从绑定值中获取最小宽度和最大宽度
const {
minWidth = 150,
maxWidth = 400,
id = '',
padding = 10,
} = binding.value || {};
// 拖拽元素内部插入一个包裹元素,方便控制所有子元素隐藏显示
const wrapper = document.createElement('div');
wrapper.style.cssText = `width: 100%; height: 100%; overflow: auto; padding:${padding}px`;
while (el.firstChild) {
// appendChild 方法用于将一个节点添加到另一个节点的子节点列表的末尾。
// 如果要添加的节点已经存在于文档树中,appendChild 方法会将该节点从其当前位置移动到新的位置,而不是复制该节点。
// 这样就可以将 el 的所有子元素通过循环全部插入至 wrapper 中
wrapper.appendChild(el.firstChild);
}
el.appendChild(wrapper);
// 创建拖拽元素
const resizer = document.createElement('div');
resizer.style.cssText =
'width: 10px; height: 100%; position: absolute; right: -10px; top: 0px; cursor: ew-resize; user-select: none; display: flex; justify-content: center; align-items: center; z-index: 999;';
el.style.position = 'relative';
el.style.padding = '0px';
el.appendChild(resizer);
el.style.transition = 'width 0.3s ease';
// 缓存宽度
const savedWidth = localStorage.getItem('WIDTH' + id);
if (savedWidth) {
el.style.width = savedWidth;
if (el.style.width === '0px') {
wrapper.style.display = 'none';
}
}
// 创建切换按钮
const img = document.createElement('img');
img.src =
el.style.width === '0px'
? requireImg('tree/7.png')
: requireImg('tree/6.png');
img.style.cssText = 'cursor:pointer;height:40px;';
resizer.appendChild(img);
// 切换显示/隐藏逻辑
img.addEventListener('mousedown', (e) => {
e.stopPropagation();
toggleContainer(el, wrapper, img);
});
// 拖拽虚线
const line = document.createElement('div');
line.style.cssText =
'position: absolute; height: 100%; width: 2px; right: 0; top: 0; z-index: 9999; border-right: 0px dashed #409EFF; pointer-events: none;';
el.appendChild(line);
// 拖拽事件
resizer.addEventListener('mousedown', () => {
document.addEventListener('mousemove', resize);
document.addEventListener('mouseup', stopResize);
});
let newWidth;
resize = function (e) {
line.style.borderRight = '2px dashed #409EFF';
// 使用传入的最小宽度和最大宽度
const width = e.pageX - el.getBoundingClientRect().left;
if (width <= 0) {
newWidth = 0;
} else {
newWidth = Math.max(minWidth, Math.min(maxWidth, width));
}
line.style.right = `${el.getBoundingClientRect().right - e.pageX}px`;
};
stopResize = function () {
line.style.borderRight = '0px dashed #409EFF';
document.removeEventListener('mousemove', resize);
document.removeEventListener('mouseup', stopResize);
if (newWidth) {
el.style.width = `${newWidth}px`;
setTimeout(() => {
wrapper.style.display = 'block';
}, 200);
} else {
el.style.width = `0px`;
wrapper.style.display = 'none';
}
img.src =
newWidth === 0 ? requireImg('tree/7.png') : requireImg('tree/6.png');
localStorage.setItem('WIDTH' + id, el.style.width);
};
function toggleContainer(el, wrapper, img) {
if (el.style.width === '0px') {
el.style.width = `${minWidth}px`;
img.src = requireImg('tree/6.png');
setTimeout(() => {
wrapper.style.display = 'block';
}, 200);
} else {
wrapper.style.display = 'none';
el.style.width = '0px';
img.src = requireImg('tree/7.png');
}
localStorage.setItem('WIDTH' + id, el.style.width);
}
},
unbind() {
// 清除所有事件监听器,防止内存泄漏
document.removeEventListener('mousemove', resize);
document.removeEventListener('mouseup', stopResize);
},
};
Vue.directive('resize', vResize);
使用指令
<div class="page">
<div
class="left"
v-resize="{
id: 'left_tree',
minWidth: '473',
maxWidth: '773',
}"
v-loading="treeloading">
...
</div>
<div class="right">
...
</div>
</div>
.page{
display:flex;
.left_tree{
width:550px;
}
.right{
flex:1;
}
}