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, collapsedButton: true,
collapsedShowTitle: false, collapsedShowTitle: false,
collapseWidth: 60, collapseWidth: 60,
draggable: true,
enable: true, enable: true,
expandOnHover: true, expandOnHover: true,
extraCollapse: false, extraCollapse: false,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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