fix: 侧边栏菜单拖拽功能在设置内增加开关

This commit is contained in:
zouawen
2026-02-28 11:19:01 +08:00
parent b2013436c5
commit 8e71261d49
10 changed files with 139 additions and 65 deletions

View File

@@ -85,6 +85,7 @@ const defaultPreferences: Preferences = {
collapsedButton: true,
collapsedShowTitle: false,
collapseWidth: 60,
draggable: true,
enable: true,
expandOnHover: true,
extraCollapse: false,

View File

@@ -170,6 +170,8 @@ interface SidebarPreferences {
collapsedShowTitle: boolean;
/** 侧边栏折叠宽度 */
collapseWidth: number;
/** 侧边栏菜单拖拽 */
draggable: boolean;
/** 侧边栏是否可见 */
enable: boolean;
/** 菜单自动展开状态 */

View File

@@ -109,6 +109,7 @@ const props = withDefaults(defineProps<Props>(), {
});
const emit = defineEmits<{ leave: []; 'update:width': [value: number] }>();
const draggable = defineModel<boolean>('draggable');
const collapse = defineModel<boolean>('collapse');
const extraCollapse = defineModel<boolean>('extraCollapse');
const expandOnHovering = defineModel<boolean>('expandOnHovering');
@@ -262,14 +263,19 @@ const handleDragSidebar = (e: MouseEvent) => {
const { isSidebarMixed, collapseWidth, extraWidth, width } = props;
const minLimit = collapseWidth;
const maxLimit = 320;
const currentWidth = isSidebarMixed ? extraWidth : width;
const startWidth = isSidebarMixed ? extraWidth : width;
startDrag(
e,
minLimit,
maxLimit,
currentWidth,
asideRef.value,
dragBarRef.value,
{
min: minLimit,
max: maxLimit,
startWidth,
},
{
target: asideRef.value,
dragBar: dragBarRef.value,
},
(newWidth) => {
emit('update:width', newWidth);
if (isSidebarMixed) {
@@ -357,6 +363,7 @@ const handleDragSidebar = (e: MouseEvent) => {
</VbenScrollbar>
</div>
<div
v-if="draggable"
ref="dragBarRef"
class="absolute inset-y-0 -right-[1px] z-1000 w-[2px] cursor-col-resize hover:bg-primary"
@mousedown="handleDragSidebar"

View File

@@ -1,106 +1,157 @@
import { onUnmounted } from 'vue';
interface DragOptions {
max: number;
min: number;
startWidth: number;
}
interface DragElements {
dragBar: HTMLElement | null;
target: HTMLElement | null;
}
type DragCallback = (newWidth: number) => void;
export function useSidebarDrag() {
let startX = 0;
let startWidth = 0;
let targetTransition = '';
let dragBarTransition = '';
let dragBarOffsetLeft = 0;
let dragBarLeft = '';
let dragBarRight = '';
let userSelect = '';
let cursor = '';
let cleanup: (() => void) | null = null;
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 startDrag = (
e: MouseEvent,
min: number,
max: number,
currentWidth: number,
targetElement: HTMLElement | null,
dragBarElement: HTMLElement | null,
onDrag: (newWidth: number) => void,
options: DragOptions,
elements: DragElements,
onDrag: DragCallback,
) => {
cleanup?.();
const { min, max, startWidth } = options;
const { dragBar, target } = elements;
if (state.isDragging || !dragBar || !target) return;
e.preventDefault();
e.stopPropagation();
if (!dragBarElement || !targetElement) return;
state.isDragging = true;
startX = e.clientX;
startWidth = currentWidth;
state.startX = e.clientX;
state.startWidth = startWidth;
state.startLeft = dragBar.offsetLeft;
targetTransition = targetElement.style.transition;
dragBarTransition = dragBarElement.style.transition;
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,
};
dragBarOffsetLeft = dragBarElement.offsetLeft;
dragBarLeft = dragBarElement.style.left;
dragBarRight = dragBarElement.style.right;
userSelect = document.body.style.userSelect;
cursor = document.body.style.cursor;
targetElement.style.transition = 'none';
dragBarElement.style.transition = 'none';
dragBarElement.style.left = `${dragBarOffsetLeft}px`;
dragBarElement.style.right = 'auto';
document.body.style.userSelect = 'none';
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
dragBar.style.left = `${state.startLeft}px`;
dragBar.style.right = 'auto';
dragBar.style.transition = 'none';
target.style.transition = 'none';
const onMouseMove = (moveEvent: MouseEvent) => {
const deltaX = moveEvent.clientX - startX;
let newLeft = dragBarOffsetLeft + deltaX;
if (!state.isDragging || !dragBar) return;
const deltaX = moveEvent.clientX - state.startX;
let newLeft = state.startLeft + deltaX;
if (newLeft < min) newLeft = min;
if (newLeft > max) newLeft = max;
dragBarElement.style.left = `${newLeft}px`;
dragBar.style.left = `${newLeft}px`;
dragBar.classList.add('bg-primary');
};
const onMouseUp = (upEvent: MouseEvent) => {
const deltaX = upEvent.clientX - startX;
let newWidth = startWidth + deltaX;
if (!state.isDragging || !dragBar || !target) return;
const deltaX = upEvent.clientX - state.startX;
let newWidth = state.startWidth + deltaX;
newWidth = Math.min(max, Math.max(min, newWidth));
if (dragBarElement) {
dragBarElement.style.left = dragBarLeft;
dragBarElement.style.right = dragBarRight;
}
dragBar.classList.remove('bg-primary');
onDrag?.(newWidth);
cleanup?.();
endDrag();
};
document.addEventListener('mousemove', onMouseMove, { passive: true });
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
cleanup = () => {
const cleanup = () => {
if (!state.cleanup) return;
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
if (targetElement) {
targetElement.style.transition = targetTransition;
}
if (dragBarElement) {
dragBarElement.style.transition = dragBarTransition;
dragBarElement.style.left = dragBarLeft;
dragBarElement.style.right = dragBarRight;
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');
}
document.body.style.userSelect = userSelect;
document.body.style.cursor = cursor;
if (target) {
target.style.transition = state.originalStyles.targetTransition;
}
cleanup = null;
state.isDragging = false;
state.cleanup = null;
};
state.cleanup = cleanup;
};
const endDrag = () => {
state.cleanup?.();
};
onUnmounted(() => {
cleanup?.();
endDrag();
});
return {
startDrag,
endDrag,
get isDragging() {
return state.isDragging;
},
};
}

View File

@@ -69,6 +69,9 @@ const emit = defineEmits<{
toggleSidebar: [];
'update:sidebar-width': [value: number];
}>();
const sidebarDraggable = defineModel<boolean>('sidebarDraggable', {
default: true,
});
const sidebarCollapse = defineModel<boolean>('sidebarCollapse', {
default: false,
});
@@ -493,6 +496,7 @@ const idMainContent = ELEMENT_ID_MAIN_CONTENT;
<div class="relative flex min-h-full w-full">
<LayoutSidebar
v-if="sidebarEnableState"
v-model:draggable="sidebarDraggable"
v-model:collapse="sidebarCollapse"
v-model:expand-on-hover="sidebarExpandOnHover"
v-model:expand-on-hovering="sidebarExpandOnHovering"

View File

@@ -234,6 +234,7 @@ const headerSlots = computed(() => {
:header-visible="preferences.header.enable"
:is-mobile="preferences.app.isMobile"
:layout="layout"
:sidebar-draggable="preferences.sidebar.draggable"
:sidebar-collapse="preferences.sidebar.collapsed"
:sidebar-collapse-show-title="preferences.sidebar.collapsedShowTitle"
:sidebar-enable="sidebarVisible"

View File

@@ -19,6 +19,7 @@ const sidebarCollapsedShowTitle = defineModel<boolean>(
const sidebarAutoActivateChild = defineModel<boolean>(
'sidebarAutoActivateChild',
);
const sidebarDraggable = defineModel<boolean>('sidebarDraggable');
const sidebarCollapsed = defineModel<boolean>('sidebarCollapsed');
const sidebarExpandOnHover = defineModel<boolean>('sidebarExpandOnHover');
@@ -48,6 +49,9 @@ const handleCheckboxChange = () => {
<SwitchItem v-model="sidebarEnable" :disabled="disabled">
{{ $t('preferences.sidebar.visible') }}
</SwitchItem>
<SwitchItem v-model="sidebarDraggable" :disabled="!sidebarEnable || disabled">
{{ $t('preferences.sidebar.draggable') }}
</SwitchItem>
<SwitchItem v-model="sidebarCollapsed" :disabled="!sidebarEnable || disabled">
{{ $t('preferences.sidebar.collapsed') }}
</SwitchItem>

View File

@@ -93,6 +93,7 @@ const themeSemiDarkHeader = defineModel<boolean>('themeSemiDarkHeader');
const sidebarEnable = defineModel<boolean>('sidebarEnable');
const sidebarWidth = defineModel<number>('sidebarWidth');
const sidebarDraggable = defineModel<boolean>('sidebarDraggable');
const sidebarCollapsed = defineModel<boolean>('sidebarCollapsed');
const sidebarCollapsedShowTitle = defineModel<boolean>(
'sidebarCollapsedShowTitle',
@@ -354,6 +355,7 @@ async function handleReset() {
<Block :title="$t('preferences.sidebar.title')">
<Sidebar
v-model:sidebar-auto-activate-child="sidebarAutoActivateChild"
v-model:sidebar-draggable="sidebarDraggable"
v-model:sidebar-collapsed="sidebarCollapsed"
v-model:sidebar-collapsed-show-title="sidebarCollapsedShowTitle"
v-model:sidebar-enable="sidebarEnable"

View File

@@ -54,6 +54,7 @@
"title": "Sidebar",
"width": "Width",
"visible": "Show Sidebar",
"draggable": "Drag Sidebar Menu",
"collapsed": "Collpase Menu",
"collapsedShowTitle": "Show Menu Title",
"autoActivateChild": "Auto Activate SubMenu",

View File

@@ -54,6 +54,7 @@
"title": "侧边栏",
"width": "宽度",
"visible": "显示侧边栏",
"draggable": "侧边栏菜单拖拽",
"collapsed": "折叠菜单",
"collapsedShowTitle": "折叠显示菜单名",
"autoActivateChild": "自动激活子菜单",