mirror of
https://github.com/imdap/ruoyi-plus-vben5.git
synced 2026-04-23 00:38:34 +08:00
fix: 侧边菜单栏拖拽优化 (#7606)
* fix: 拖拽使用遮罩层实现,使得拖拽到min或max临界值时cursor显示not-allowed,同时拖拽线条颜色变浅,类似于disabled,提升用户体验 * fix: 修复代码审查建议;修复lint和test报错 * fix: 修复遮罩层挡住hover:bg-primary视觉效果问题
This commit is contained in:
@@ -20,8 +20,8 @@ exports[`defaultPreferences immutability test > should not modify the config obj
|
||||
"defaultHomePath": "/analytics",
|
||||
"dynamicTitle": true,
|
||||
"enableCheckUpdates": true,
|
||||
"enablePreferences": true,
|
||||
"enableCopyPreferences": true,
|
||||
"enablePreferences": true,
|
||||
"enableRefreshToken": false,
|
||||
"enableStickyPreferencesNavigationBar": true,
|
||||
"isMobile": false,
|
||||
|
||||
@@ -53,10 +53,10 @@ interface AppPreferences {
|
||||
dynamicTitle: boolean;
|
||||
/** 是否开启检查更新 */
|
||||
enableCheckUpdates: boolean;
|
||||
/** 是否显示偏好设置 */
|
||||
enablePreferences: boolean;
|
||||
/** 是否显示复制偏好设置按钮 */
|
||||
enableCopyPreferences: boolean;
|
||||
/** 是否显示偏好设置 */
|
||||
enablePreferences: boolean;
|
||||
/**
|
||||
* @zh_CN 是否开启refreshToken
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { CSSProperties } from 'vue';
|
||||
|
||||
import { computed, shallowRef, useSlots, watchEffect } from 'vue';
|
||||
import { computed, onUnmounted, shallowRef, useSlots, watchEffect } from 'vue';
|
||||
|
||||
import { VbenScrollbar } from '@vben-core/shadcn-ui';
|
||||
|
||||
@@ -262,20 +262,18 @@ function handleMouseleave() {
|
||||
extraVisible.value = false;
|
||||
}
|
||||
|
||||
const { startDrag } = useSidebarDrag();
|
||||
const { startDrag, endDrag } = useSidebarDrag();
|
||||
|
||||
const handleDragSidebar = (e: MouseEvent) => {
|
||||
const { isSidebarMixed, collapseWidth, extraWidth, width } = props;
|
||||
const { isSidebarMixed, collapseWidth, width } = props;
|
||||
const minLimit = isSidebarMixed ? width + collapseWidth : collapseWidth;
|
||||
const maxLimit = isSidebarMixed ? width + 320 : 320;
|
||||
const startWidth = isSidebarMixed ? width + extraWidth : width;
|
||||
|
||||
startDrag(
|
||||
e,
|
||||
{
|
||||
min: minLimit,
|
||||
max: maxLimit,
|
||||
startWidth,
|
||||
},
|
||||
{
|
||||
target: asideRef.value,
|
||||
@@ -293,6 +291,10 @@ const handleDragSidebar = (e: MouseEvent) => {
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
endDrag();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { onUnmounted } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
|
||||
interface DragOptions {
|
||||
max: number;
|
||||
min: number;
|
||||
startWidth: number;
|
||||
}
|
||||
|
||||
interface DragElements {
|
||||
@@ -14,35 +13,9 @@ interface DragElements {
|
||||
type DragCallback = (newWidth: number) => void;
|
||||
|
||||
export function useSidebarDrag() {
|
||||
const state: {
|
||||
cleanup: (() => void) | null;
|
||||
isDragging: boolean;
|
||||
originalStyles: {
|
||||
bodyCursor: string;
|
||||
bodyUserSelect: string;
|
||||
dragBarLeft: string;
|
||||
dragBarRight: string;
|
||||
dragBarTransition: string;
|
||||
targetTransition: string;
|
||||
};
|
||||
startLeft: number;
|
||||
startWidth: number;
|
||||
startX: number;
|
||||
} = {
|
||||
cleanup: null,
|
||||
isDragging: false,
|
||||
startLeft: 0,
|
||||
startWidth: 0,
|
||||
startX: 0,
|
||||
originalStyles: {
|
||||
bodyCursor: '',
|
||||
bodyUserSelect: '',
|
||||
dragBarLeft: '',
|
||||
dragBarRight: '',
|
||||
dragBarTransition: '',
|
||||
targetTransition: '',
|
||||
},
|
||||
};
|
||||
const isDragging = ref(false);
|
||||
let cleanup: (() => void) | null = null;
|
||||
let dragOverlay: HTMLElement | null = null;
|
||||
|
||||
const startDrag = (
|
||||
e: MouseEvent,
|
||||
@@ -50,108 +23,130 @@ export function useSidebarDrag() {
|
||||
elements: DragElements,
|
||||
onDrag: DragCallback,
|
||||
) => {
|
||||
const { min, max, startWidth } = options;
|
||||
const { min, max } = options;
|
||||
const { dragBar, target } = elements;
|
||||
|
||||
if (state.isDragging || !dragBar || !target) return;
|
||||
if (isDragging.value || !dragBar || !target) return;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
state.isDragging = true;
|
||||
isDragging.value = true;
|
||||
|
||||
state.startX = e.clientX;
|
||||
state.startWidth = startWidth;
|
||||
state.startLeft = dragBar.offsetLeft;
|
||||
const startX = e.clientX;
|
||||
const startWidth = target.getBoundingClientRect().width;
|
||||
const startLeft = dragBar.offsetLeft;
|
||||
|
||||
state.originalStyles = {
|
||||
bodyCursor: document.body.style.cursor,
|
||||
bodyUserSelect: document.body.style.userSelect,
|
||||
dragBarLeft: dragBar.style.left,
|
||||
dragBarRight: dragBar.style.right,
|
||||
dragBarTransition: dragBar.style.transition,
|
||||
targetTransition: target.style.transition,
|
||||
};
|
||||
dragBar.classList.add('bg-primary');
|
||||
dragBar.classList.remove('bg-primary/30');
|
||||
|
||||
document.body.style.cursor = 'col-resize';
|
||||
document.body.style.userSelect = 'none';
|
||||
const dragBarTransition = dragBar.style.transition;
|
||||
const targetTransition = target.style.transition;
|
||||
|
||||
dragBar.style.left = `${state.startLeft}px`;
|
||||
dragBar.style.right = 'auto';
|
||||
dragBar.style.transition = 'none';
|
||||
target.style.transition = 'none';
|
||||
|
||||
dragOverlay = document.createElement('div');
|
||||
dragOverlay.style.position = 'fixed';
|
||||
dragOverlay.style.inset = '0';
|
||||
dragOverlay.style.zIndex = '9999';
|
||||
dragOverlay.style.cursor = 'col-resize';
|
||||
dragOverlay.style.userSelect = 'none';
|
||||
dragOverlay.style.outline = 'none';
|
||||
dragOverlay.tabIndex = -1;
|
||||
dragOverlay.style.background = 'rgba(0,0,0,0)';
|
||||
document.body.append(dragOverlay);
|
||||
|
||||
const onMouseMove = (moveEvent: MouseEvent) => {
|
||||
if (!state.isDragging || !dragBar) return;
|
||||
if (!isDragging.value || !dragBar || !target) {
|
||||
endDrag();
|
||||
return;
|
||||
}
|
||||
|
||||
const deltaX = moveEvent.clientX - state.startX;
|
||||
let newLeft = state.startLeft + deltaX;
|
||||
const deltaX = moveEvent.clientX - startX;
|
||||
let currentWidth = startWidth + deltaX;
|
||||
|
||||
if (newLeft < min) newLeft = min;
|
||||
if (newLeft > max) newLeft = max;
|
||||
const isOutOfMin = currentWidth < min;
|
||||
const isOutOfMax = currentWidth > max;
|
||||
const isOutOfBounds = isOutOfMin || isOutOfMax;
|
||||
|
||||
if (isOutOfMin) currentWidth = min;
|
||||
if (isOutOfMax) currentWidth = max;
|
||||
|
||||
const newLeft = startLeft + (currentWidth - startWidth);
|
||||
|
||||
if (dragOverlay)
|
||||
dragOverlay.style.cursor = isOutOfBounds ? 'not-allowed' : 'col-resize';
|
||||
|
||||
dragBar.style.left = `${newLeft}px`;
|
||||
dragBar.classList.add('bg-primary');
|
||||
|
||||
if (isOutOfBounds) {
|
||||
dragBar.classList.add('bg-primary/30');
|
||||
dragBar.classList.remove('bg-primary');
|
||||
} else {
|
||||
dragBar.classList.add('bg-primary');
|
||||
dragBar.classList.remove('bg-primary/30');
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseUp = (upEvent: MouseEvent) => {
|
||||
if (!state.isDragging || !dragBar || !target) return;
|
||||
if (!isDragging.value || !dragBar || !target) {
|
||||
endDrag();
|
||||
return;
|
||||
}
|
||||
|
||||
const deltaX = upEvent.clientX - state.startX;
|
||||
let newWidth = state.startWidth + deltaX;
|
||||
const deltaX = upEvent.clientX - startX;
|
||||
let newWidth = startWidth + deltaX;
|
||||
|
||||
newWidth = Math.min(max, Math.max(min, newWidth));
|
||||
|
||||
dragBar.classList.remove('bg-primary');
|
||||
dragBar.classList.remove('bg-primary', 'bg-primary/30');
|
||||
|
||||
onDrag?.(newWidth);
|
||||
|
||||
endDrag();
|
||||
try {
|
||||
onDrag?.(Math.round(newWidth));
|
||||
} finally {
|
||||
endDrag();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', onMouseMove);
|
||||
document.addEventListener('mouseup', onMouseUp);
|
||||
|
||||
const cleanup = () => {
|
||||
if (!state.cleanup) return;
|
||||
cleanup = () => {
|
||||
if (!cleanup) return;
|
||||
|
||||
document.removeEventListener('mousemove', onMouseMove);
|
||||
document.removeEventListener('mouseup', onMouseUp);
|
||||
|
||||
document.body.style.cursor = state.originalStyles.bodyCursor;
|
||||
document.body.style.userSelect = state.originalStyles.bodyUserSelect;
|
||||
|
||||
if (dragBar) {
|
||||
dragBar.style.left = state.originalStyles.dragBarLeft;
|
||||
dragBar.style.right = state.originalStyles.dragBarRight;
|
||||
dragBar.style.transition = state.originalStyles.dragBarTransition;
|
||||
dragBar.classList.remove('bg-primary');
|
||||
dragBar.style.transition = dragBarTransition;
|
||||
dragBar.style.left = '';
|
||||
dragBar.classList.remove('bg-primary', 'bg-primary/30');
|
||||
}
|
||||
|
||||
if (target) {
|
||||
target.style.transition = state.originalStyles.targetTransition;
|
||||
target.style.transition = targetTransition;
|
||||
}
|
||||
|
||||
state.isDragging = false;
|
||||
state.cleanup = null;
|
||||
};
|
||||
if (dragOverlay) {
|
||||
dragOverlay.remove();
|
||||
dragOverlay = null;
|
||||
}
|
||||
|
||||
state.cleanup = cleanup;
|
||||
isDragging.value = false;
|
||||
cleanup = null;
|
||||
};
|
||||
};
|
||||
|
||||
const endDrag = () => {
|
||||
state.cleanup?.();
|
||||
cleanup?.();
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
endDrag();
|
||||
});
|
||||
|
||||
return {
|
||||
startDrag,
|
||||
endDrag,
|
||||
get isDragging() {
|
||||
return state.isDragging;
|
||||
return isDragging.value;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user