diff --git a/package.json b/package.json index d5e8f4e8..d443dbec 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vben-admin-monorepo", - "version": "5.5.9", + "version": "5.6.0", "private": true, "keywords": [ "monorepo", diff --git a/packages/@core/base/typings/src/vue-router.d.ts b/packages/@core/base/typings/src/vue-router.d.ts index 10c97f62..9817cc29 100644 --- a/packages/@core/base/typings/src/vue-router.d.ts +++ b/packages/@core/base/typings/src/vue-router.d.ts @@ -43,6 +43,10 @@ interface RouteMeta { | 'success' | 'warning' | string; + /** + * 路由对应dom是否缓存起来 + */ + domCached?: boolean; /** * 路由的完整路径作为key(默认true) */ diff --git a/packages/@core/preferences/__tests__/__snapshots__/config.test.ts.snap b/packages/@core/preferences/__tests__/__snapshots__/config.test.ts.snap index 46f54a15..e4ed4cd0 100644 --- a/packages/@core/preferences/__tests__/__snapshots__/config.test.ts.snap +++ b/packages/@core/preferences/__tests__/__snapshots__/config.test.ts.snap @@ -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, diff --git a/packages/@core/preferences/src/config.ts b/packages/@core/preferences/src/config.ts index 0bdf5c65..08cb210e 100644 --- a/packages/@core/preferences/src/config.ts +++ b/packages/@core/preferences/src/config.ts @@ -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, diff --git a/packages/@core/preferences/src/types.ts b/packages/@core/preferences/src/types.ts index 09f48a17..9c8f95d8 100644 --- a/packages/@core/preferences/src/types.ts +++ b/packages/@core/preferences/src/types.ts @@ -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; /** 菜单自动展开状态 */ diff --git a/packages/@core/ui-kit/layout-ui/src/components/layout-sidebar.vue b/packages/@core/ui-kit/layout-ui/src/components/layout-sidebar.vue index 76ba74d1..e717c905 100644 --- a/packages/@core/ui-kit/layout-ui/src/components/layout-sidebar.vue +++ b/packages/@core/ui-kit/layout-ui/src/components/layout-sidebar.vue @@ -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(), { zIndex: 0, }); -const emit = defineEmits<{ leave: [] }>(); +const emit = defineEmits<{ leave: []; 'update:width': [value: number] }>(); +const draggable = defineModel('draggable'); const collapse = defineModel('collapse'); const extraCollapse = defineModel('extraCollapse'); const expandOnHovering = defineModel('expandOnHovering'); @@ -117,8 +119,8 @@ const extraVisible = defineModel('extraVisible'); const isLocked = useScrollLock(document.body); const slots = useSlots(); -// @ts-expect-error unused -const asideRef = shallowRef(); +const asideRef = shallowRef(null); +const dragBarRef = shallowRef(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; + } + }, + ); +}; diff --git a/packages/@core/ui-kit/layout-ui/src/hooks/use-sidebar-drag.ts b/packages/@core/ui-kit/layout-ui/src/hooks/use-sidebar-drag.ts new file mode 100644 index 00000000..c7246889 --- /dev/null +++ b/packages/@core/ui-kit/layout-ui/src/hooks/use-sidebar-drag.ts @@ -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; + }, + }; +} diff --git a/packages/@core/ui-kit/layout-ui/src/vben-layout.vue b/packages/@core/ui-kit/layout-ui/src/vben-layout.vue index b51be1e6..7e444068 100644 --- a/packages/@core/ui-kit/layout-ui/src/vben-layout.vue +++ b/packages/@core/ui-kit/layout-ui/src/vben-layout.vue @@ -64,7 +64,14 @@ const props = withDefaults(defineProps(), { zIndex: 200, }); -const emit = defineEmits<{ sideMouseLeave: []; toggleSidebar: [] }>(); +const emit = defineEmits<{ + sideMouseLeave: []; + toggleSidebar: []; + 'update:sidebar-width': [value: number]; +}>(); +const sidebarDraggable = defineModel('sidebarDraggable', { + default: true, +}); const sidebarCollapse = defineModel('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;