This commit is contained in:
dap
2026-03-04 11:01:52 +08:00
25 changed files with 576 additions and 107 deletions

View File

@@ -43,6 +43,10 @@ interface RouteMeta {
| 'success'
| 'warning'
| string;
/**
* 路由对应dom是否缓存起来
*/
domCached?: boolean;
/**
* 路由的完整路径作为key默认true
*/

View File

@@ -21,6 +21,7 @@ exports[`defaultPreferences immutability test > should not modify the config obj
"dynamicTitle": true,
"enableCheckUpdates": true,
"enablePreferences": true,
"enableCopyPreferences": true,
"enableRefreshToken": false,
"enableStickyPreferencesNavigationBar": true,
"isMobile": false,
@@ -84,6 +85,7 @@ exports[`defaultPreferences immutability test > should not modify the config obj
"collapsed": false,
"collapsedButton": true,
"collapsedShowTitle": false,
"draggable": true,
"enable": true,
"expandOnHover": true,
"extraCollapse": false,

View File

@@ -21,6 +21,7 @@ const defaultPreferences: Preferences = {
dynamicTitle: true,
enableCheckUpdates: true,
enablePreferences: true,
enableCopyPreferences: true,
enableRefreshToken: false,
enableStickyPreferencesNavigationBar: true,
isMobile: false,
@@ -85,6 +86,7 @@ const defaultPreferences: Preferences = {
collapsedButton: true,
collapsedShowTitle: false,
collapseWidth: 60,
draggable: true,
enable: true,
expandOnHover: true,
extraCollapse: false,

View File

@@ -55,6 +55,8 @@ interface AppPreferences {
enableCheckUpdates: boolean;
/** 是否显示偏好设置 */
enablePreferences: boolean;
/** 是否显示复制偏好设置按钮 */
enableCopyPreferences: boolean;
/**
* @zh_CN 是否开启refreshToken
*/
@@ -170,6 +172,8 @@ interface SidebarPreferences {
collapsedShowTitle: boolean;
/** 侧边栏折叠宽度 */
collapseWidth: number;
/** 侧边栏菜单拖拽 */
draggable: boolean;
/** 侧边栏是否可见 */
enable: boolean;
/** 菜单自动展开状态 */

View File

@@ -7,6 +7,7 @@ import { VbenScrollbar } from '@vben-core/shadcn-ui';
import { useScrollLock } from '@vueuse/core';
import { useSidebarDrag } from '../hooks/use-sidebar-drag';
import { SidebarCollapseButton, SidebarFixedButton } from './widgets';
interface Props {
@@ -107,7 +108,8 @@ const props = withDefaults(defineProps<Props>(), {
zIndex: 0,
});
const emit = defineEmits<{ leave: [] }>();
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');
@@ -117,8 +119,8 @@ const extraVisible = defineModel<boolean>('extraVisible');
const isLocked = useScrollLock(document.body);
const slots = useSlots();
// @ts-expect-error unused
const asideRef = shallowRef<HTMLDivElement | null>();
const asideRef = shallowRef<HTMLElement | null>(null);
const dragBarRef = shallowRef<HTMLElement | null>(null);
const hiddenSideStyle = computed((): CSSProperties => calcMenuWidthStyle(true));
@@ -155,9 +157,9 @@ const extraTitleStyle = computed((): CSSProperties => {
});
const contentWidthStyle = computed((): CSSProperties => {
const { collapseWidth, fixedExtra, isSidebarMixed, mixedWidth } = props;
const { fixedExtra, isSidebarMixed, mixedWidth } = props;
if (isSidebarMixed && fixedExtra) {
return { width: `${collapse.value ? collapseWidth : mixedWidth}px` };
return { width: `${mixedWidth}px` };
}
return {};
});
@@ -200,19 +202,24 @@ watchEffect(() => {
});
function calcMenuWidthStyle(isHiddenDom: boolean): CSSProperties {
const { extraWidth, fixedExtra, isSidebarMixed, show, width } = props;
const {
collapseWidth,
extraWidth,
mixedWidth,
fixedExtra,
isSidebarMixed,
show,
width,
} = props;
let widthValue =
width === 0
? '0px'
: `${width + (isSidebarMixed && fixedExtra && extraVisible.value ? extraWidth : 0)}px`;
const { collapseWidth } = props;
if (isHiddenDom && expandOnHovering.value && !expandOnHover.value) {
widthValue = `${collapseWidth}px`;
widthValue = isSidebarMixed ? `${mixedWidth}px` : `${collapseWidth}px`;
}
return {
...(widthValue === '0px' ? { overflow: 'hidden' } : {}),
flex: `0 0 ${widthValue}`,
@@ -254,6 +261,38 @@ function handleMouseleave() {
collapse.value = true;
extraVisible.value = false;
}
const { startDrag } = useSidebarDrag();
const handleDragSidebar = (e: MouseEvent) => {
const { isSidebarMixed, collapseWidth, extraWidth, 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,
dragBar: dragBarRef.value,
},
(newWidth) => {
if (isSidebarMixed) {
emit('update:width', newWidth - width);
extraCollapse.value = collapse.value =
newWidth - width <= collapseWidth;
} else {
emit('update:width', newWidth);
collapse.value = extraCollapse.value = newWidth <= collapseWidth;
}
},
);
};
</script>
<template>
@@ -264,6 +303,7 @@ function handleMouseleave() {
class="h-full transition-all duration-150"
></div>
<aside
ref="asideRef"
:style="style"
class="fixed left-0 top-0 h-full transition-all duration-150"
@mouseenter="handleMouseenter"
@@ -299,7 +339,6 @@ function handleMouseleave() {
</div>
<div
v-if="isSidebarMixed"
ref="asideRef"
:class="[
themeSub,
{
@@ -330,5 +369,11 @@ function handleMouseleave() {
<slot name="extra"></slot>
</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"
></div>
</aside>
</template>

View File

@@ -0,0 +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() {
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,
options: DragOptions,
elements: DragElements,
onDrag: DragCallback,
) => {
const { min, max, startWidth } = options;
const { dragBar, target } = elements;
if (state.isDragging || !dragBar || !target) return;
e.preventDefault();
e.stopPropagation();
state.isDragging = true;
state.startX = e.clientX;
state.startWidth = startWidth;
state.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,
};
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) => {
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;
dragBar.style.left = `${newLeft}px`;
dragBar.classList.add('bg-primary');
};
const onMouseUp = (upEvent: MouseEvent) => {
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));
dragBar.classList.remove('bg-primary');
onDrag?.(newWidth);
endDrag();
};
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
const cleanup = () => {
if (!state.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');
}
if (target) {
target.style.transition = state.originalStyles.targetTransition;
}
state.isDragging = false;
state.cleanup = null;
};
state.cleanup = cleanup;
};
const endDrag = () => {
state.cleanup?.();
};
onUnmounted(() => {
endDrag();
});
return {
startDrag,
endDrag,
get isDragging() {
return state.isDragging;
},
};
}

View File

@@ -64,7 +64,14 @@ const props = withDefaults(defineProps<Props>(), {
zIndex: 200,
});
const emit = defineEmits<{ sideMouseLeave: []; toggleSidebar: [] }>();
const emit = defineEmits<{
sideMouseLeave: [];
toggleSidebar: [];
'update:sidebar-width': [value: number];
}>();
const sidebarDraggable = defineModel<boolean>('sidebarDraggable', {
default: true,
});
const sidebarCollapse = defineModel<boolean>('sidebarCollapse', {
default: false,
});
@@ -120,13 +127,16 @@ const headerWrapperHeight = computed(() => {
});
const getSideCollapseWidth = computed(() => {
const { sidebarCollapseShowTitle, sidebarMixedWidth, sideCollapseWidth } =
props;
const {
sidebarCollapseShowTitle,
sidebarExtraCollapsedWidth,
sideCollapseWidth,
} = props;
return sidebarCollapseShowTitle ||
isSidebarMixedNav.value ||
isHeaderMixedNav.value
? sidebarMixedWidth
? sidebarExtraCollapsedWidth
: sideCollapseWidth;
});
@@ -237,9 +247,7 @@ const mainStyle = computed(() => {
sidebarExtraVisible.value;
if (isSideNavEffective) {
const sideCollapseWidth = sidebarCollapse.value
? getSideCollapseWidth.value
: props.sidebarMixedWidth;
const sideCollapseWidth = props.sidebarMixedWidth;
const sideWidth = sidebarExtraCollapse.value
? props.sidebarExtraCollapsedWidth
: props.sidebarWidth;
@@ -248,10 +256,14 @@ const mainStyle = computed(() => {
sidebarAndExtraWidth = `${sideCollapseWidth + sideWidth}px`;
width = `calc(100% - ${sidebarAndExtraWidth})`;
} else {
sidebarAndExtraWidth =
sidebarExpandOnHovering.value && !sidebarExpandOnHover.value
? `${getSideCollapseWidth.value}px`
: `${getSidebarWidth.value}px`;
let sidebarWidth = getSidebarWidth.value;
if (sidebarExpandOnHovering.value && !sidebarExpandOnHover.value) {
sidebarWidth =
isSidebarMixedNav.value || isHeaderMixedNav.value
? props.sidebarMixedWidth
: getSideCollapseWidth.value;
}
sidebarAndExtraWidth = `${sidebarWidth}px`;
width = `calc(100% - ${sidebarAndExtraWidth})`;
}
}
@@ -486,6 +498,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"
@@ -507,6 +520,7 @@ const idMainContent = ELEMENT_ID_MAIN_CONTENT;
:width="getSidebarWidth"
:z-index="sidebarZIndex"
@leave="() => emit('sideMouseLeave')"
@update:width="(val) => emit('update:sidebar-width', val)"
>
<template v-if="isSideMode && !isMixedNav" #logo>
<slot name="logo"></slot>