Browse Source

fix: 侧边菜单栏拖拽优化 (#7606)

* fix: 拖拽使用遮罩层实现,使得拖拽到min或max临界值时cursor显示not-allowed,同时拖拽线条颜色变浅,类似于disabled,提升用户体验

* fix: 修复代码审查建议;修复lint和test报错

* fix: 修复遮罩层挡住hover:bg-primary视觉效果问题
pull/7616/head
zouawen 2 weeks ago
committed by GitHub
parent
commit
aa7d8630b5
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      packages/@core/preferences/__tests__/__snapshots__/config.test.ts.snap
  2. 4
      packages/@core/preferences/src/types.ts
  3. 12
      packages/@core/ui-kit/layout-ui/src/components/layout-sidebar.vue
  4. 155
      packages/@core/ui-kit/layout-ui/src/hooks/use-sidebar-drag.ts
  5. 4
      packages/effects/layouts/src/widgets/preferences/blocks/general/general.vue
  6. 4
      packages/effects/layouts/src/widgets/preferences/preferences-drawer.vue

2
packages/@core/preferences/__tests__/__snapshots__/config.test.ts.snap

@ -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,

4
packages/@core/preferences/src/types.ts

@ -53,10 +53,10 @@ interface AppPreferences {
dynamicTitle: boolean;
/** 是否开启检查更新 */
enableCheckUpdates: boolean;
/** 是否显示偏好设置 */
enablePreferences: boolean;
/** 是否显示复制偏好设置按钮 */
enableCopyPreferences: boolean;
/** 是否显示偏好设置 */
enablePreferences: boolean;
/**
* @zh_CN refreshToken
*/

12
packages/@core/ui-kit/layout-ui/src/components/layout-sidebar.vue

@ -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>

155
packages/@core/ui-kit/layout-ui/src/hooks/use-sidebar-drag.ts

@ -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 - startX;
let currentWidth = startWidth + deltaX;
const isOutOfMin = currentWidth < min;
const isOutOfMax = currentWidth > max;
const isOutOfBounds = isOutOfMin || isOutOfMax;
const deltaX = moveEvent.clientX - state.startX;
let newLeft = state.startLeft + deltaX;
if (isOutOfMin) currentWidth = min;
if (isOutOfMax) currentWidth = max;
if (newLeft < min) newLeft = min;
if (newLeft > max) newLeft = max;
const newLeft = startLeft + (currentWidth - startWidth);
if (dragOverlay)
dragOverlay.style.cursor = isOutOfBounds ? 'not-allowed' : 'col-resize';
dragBar.style.left = `${newLeft}px`;
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');
onDrag?.(newWidth);
dragBar.classList.remove('bg-primary', 'bg-primary/30');
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;
},
};
}

4
packages/effects/layouts/src/widgets/preferences/blocks/general/general.vue

@ -15,7 +15,9 @@ const appDynamicTitle = defineModel<boolean>('appDynamicTitle');
const appWatermark = defineModel<boolean>('appWatermark');
const appWatermarkContent = defineModel<string>('appWatermarkContent');
const appEnableCheckUpdates = defineModel<boolean>('appEnableCheckUpdates');
const appEnableCopyPreferences = defineModel<boolean>('appEnableCopyPreferences');
const appEnableCopyPreferences = defineModel<boolean>(
'appEnableCopyPreferences',
);
</script>
<template>

4
packages/effects/layouts/src/widgets/preferences/preferences-drawer.vue

@ -70,7 +70,9 @@ const appContentCompact = defineModel<ContentCompactType>('appContentCompact');
const appWatermark = defineModel<boolean>('appWatermark');
const appWatermarkContent = defineModel<string>('appWatermarkContent');
const appEnableCheckUpdates = defineModel<boolean>('appEnableCheckUpdates');
const appEnableCopyPreferences = defineModel<boolean>('appEnableCopyPreferences');
const appEnableCopyPreferences = defineModel<boolean>(
'appEnableCopyPreferences',
);
const appEnableStickyPreferencesNavigationBar = defineModel<boolean>(
'appEnableStickyPreferencesNavigationBar',
);

Loading…
Cancel
Save