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