import type { App, Directive } from 'vue';
// 允许在 HTMLElement 上挂载清理与初始化标记
declare global {
interface HTMLElement {
_cleanup?: () => void;
_resizableInited?: boolean;
}
}
/**
* 核心初始化逻辑
*/
export function initResizable(target: HTMLElement) {
const computedStyle = window.getComputedStyle(target);
if (computedStyle.position === 'static') {
target.style.position = 'relative';
}
target.style.overflow = 'hidden';
// 拖拽手柄
type Dir = 'bottom' | 'left' | 'right' | 'top';
const handles: Partial<Record<Dir, HTMLDivElement>> = {};
const cssMap: Record<'bottom' | 'left' | 'right' | 'top', string> = {
top: 'position:absolute; top:0; left:0; width:100%; height:8px; background:transparent; cursor:ns-resize; z-index:9999; border:none !important; pointer-events:auto;',
right:
'position:absolute; top:0; right:0; width:8px; height:100%; background:transparent; cursor:ew-resize; z-index:9999; border:none !important; pointer-events:auto;',
bottom:
'position:absolute; bottom:0; left:0; width:100%; height:8px; background:transparent; cursor:ns-resize; z-index:9999; border:none !important; pointer-events:auto;',
left: 'position:absolute; top:0; left:0; width:8px; height:100%; background:transparent; cursor:ew-resize; z-index:9999; border:none !important; pointer-events:auto;',
};
// 仅在模态类元素上追加右/下手柄
const isModal =
target.classList.contains('z-popup') ||
target.classList.contains('el-dialog');
const dirsToAppend: readonly Dir[] = isModal
? (['right', 'bottom'] as const)
: (['top', 'right', 'bottom', 'left'] as const);
dirsToAppend.forEach((dir) => {
const handle = document.createElement('div');
handle.style.cssText = cssMap[dir];
handles[dir] = handle;
target.append(handle);
});
let resizingDir: '' | Dir = '';
const start = { x: 0, y: 0, width: 0, height: 0, left: 0, top: 0 };
// 记录起始状态
dirsToAppend.forEach((dir) => {
const handle = handles[dir];
if (!handle) return;
handle.addEventListener('mousedown', (e: MouseEvent) => {
resizingDir = dir;
start.x = e.clientX;
start.y = e.clientY;
start.width = target.offsetWidth;
start.height = target.offsetHeight;
start.left = target.offsetLeft;
start.top = target.offsetTop;
e.stopPropagation();
e.preventDefault();
});
});
// 鼠标移动时仅修改宽高
const handleMouseMove = (e: MouseEvent) => {
if (!resizingDir) return;
if (isModal) {
// Modal/ElDialog,只支持右与下方向,避免上/左漂移
switch (resizingDir) {
case 'bottom': {
const newH = start.height + (e.clientY - start.y);
if (newH >= 300) target.style.height = `${newH}px`;
break;
}
case 'right': {
const newW = start.width + (e.clientX - start.x);
if (newW >= 300) target.style.width = `${newW}px`;
break;
}
}
} else {
// 普通容器,四向拉伸
switch (resizingDir) {
case 'bottom': {
const newH = start.height + (e.clientY - start.y);
if (newH >= 50) target.style.height = `${newH}px`;
break;
}
case 'left': {
const newLeftW = start.width + (start.x - e.clientX);
if (newLeftW >= 50) {
target.style.width = `${newLeftW}px`;
target.style.left = `${start.left - (start.x - e.clientX)}px`;
}
break;
}
case 'right': {
const newW = start.width + (e.clientX - start.x);
if (newW >= 50) target.style.width = `${newW}px`;
break;
}
case 'top': {
const newTopH = start.height + (start.y - e.clientY);
if (newTopH >= 50) {
target.style.height = `${newTopH}px`;
target.style.top = `${start.top - (start.y - e.clientY)}px`;
}
break;
}
}
}
e.stopPropagation();
e.preventDefault();
};
document.addEventListener('mousemove', handleMouseMove);
const handleMouseUp = () => {
resizingDir = '';
};
document.addEventListener('mouseup', handleMouseUp);
// 清理逻辑
target._cleanup = () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
Object.values(handles).forEach((handle) => handle.remove());
};
}
/**
* 全局指令:v-resizable
* 已在Modal中进行全局配置,保证在Modal中生效
*/
export function registerResizableDirective(app: App) {
const directive: Directive<HTMLElement, void> = {
mounted(el) {
try {
initResizable(el);
} catch (error) {
console.warn('[v-resizable] init failed:', error);
}
},
unmounted(el) {
el._cleanup?.();
},
};
app.directive('resizable', directive);
}
指令代码如上,以上代码已将指令全局配置给Vben中的Modal模态框。详细可见我上一篇帖子Vben Admin中实现Modal可拉伸功能
在registerResizableDirective中末尾加入以下代码
// 注册时即开启对ElDialog的全局绑定
const bindDialogs = () => {
document
.querySelectorAll('.el-overlay .el-dialog, .el-dialog')
.forEach((d) => {
const h = d as HTMLElement;
if (!h.dataset.resizableBound) {
h.dataset.resizableBound = '1';
initResizable(h);
}
});
};
bindDialogs();
new MutationObserver(() => bindDialogs()).observe(document.body, {
childList: true,
subtree: true,
});
即可为ElDialog全局配置可拉伸。
以上代码非最终代码,存在多处可改进的地方,后期经过几次修改修复了其中一些问题。比如,拉伸指令用在modal中时会有拉伸动画异常的问题,需要在加监听器时加上target.style.transition = 'none';
并且使用offsetWidth来计算偏移量在上下拖拽时出现“瞬移”的情况,所以后期使用computedStyle进行了替换。
除此之外代码还存在手柄先创建后使用等问题,这里不一一赘述,有需要的可以自行修改
337

被折叠的 条评论
为什么被折叠?



