mirror of
https://gitee.com/dapppp/ruoyi-plus-vben5.git
synced 2026-03-07 23:31:08 +08:00
Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into antdv-next
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vben-admin-monorepo",
|
||||
"version": "5.5.9",
|
||||
"version": "5.6.0",
|
||||
"private": true,
|
||||
"keywords": [
|
||||
"monorepo",
|
||||
|
||||
@@ -43,6 +43,10 @@ interface RouteMeta {
|
||||
| 'success'
|
||||
| 'warning'
|
||||
| string;
|
||||
/**
|
||||
* 路由对应dom是否缓存起来
|
||||
*/
|
||||
domCached?: boolean;
|
||||
/**
|
||||
* 路由的完整路径作为key(默认true)
|
||||
*/
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
/** 菜单自动展开状态 */
|
||||
|
||||
@@ -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>
|
||||
|
||||
157
packages/@core/ui-kit/layout-ui/src/hooks/use-sidebar-drag.ts
Normal file
157
packages/@core/ui-kit/layout-ui/src/hooks/use-sidebar-drag.ts
Normal 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;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VNode } from 'vue';
|
||||
import type {
|
||||
RouteLocationNormalizedLoaded,
|
||||
RouteLocationNormalizedLoadedGeneric,
|
||||
} from 'vue-router';
|
||||
import type { RouteLocationNormalizedLoadedGeneric } from 'vue-router';
|
||||
|
||||
import { computed } from 'vue';
|
||||
import { unref } from 'vue';
|
||||
import { RouterView } from 'vue-router';
|
||||
|
||||
import { preferences, usePreferences } from '@vben/preferences';
|
||||
import { usePreferences } from '@vben/preferences';
|
||||
import { getTabKey, storeToRefs, useTabbarStore } from '@vben/stores';
|
||||
|
||||
import { transformComponent, useLayoutHook } from '../../hooks';
|
||||
import { IFrameRouterView } from '../../iframe';
|
||||
import { RouteCachedPage, RouteCachedView } from '../../route-cached';
|
||||
|
||||
defineOptions({ name: 'LayoutContent' });
|
||||
|
||||
@@ -21,85 +19,27 @@ const { keepAlive } = usePreferences();
|
||||
const { getCachedTabs, getExcludeCachedTabs, renderRouteView } =
|
||||
storeToRefs(tabbarStore);
|
||||
|
||||
/**
|
||||
* 是否使用动画
|
||||
*/
|
||||
const getEnabledTransition = computed(() => {
|
||||
const { transition } = preferences;
|
||||
const transitionName = transition.name;
|
||||
return transitionName && transition.enable;
|
||||
});
|
||||
|
||||
// 页面切换动画
|
||||
function getTransitionName(_route: RouteLocationNormalizedLoaded) {
|
||||
// 如果偏好设置未设置,则不使用动画
|
||||
const { tabbar, transition } = preferences;
|
||||
const transitionName = transition.name;
|
||||
if (!transitionName || !transition.enable) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 标签页未启用或者未开启缓存,则使用全局配置动画
|
||||
if (!tabbar.enable || !keepAlive) {
|
||||
return transitionName;
|
||||
}
|
||||
|
||||
// 如果页面已经加载过,则不使用动画
|
||||
// if (route.meta.loaded) {
|
||||
// return;
|
||||
// }
|
||||
// 已经打开且已经加载过的页面不使用动画
|
||||
// const inTabs = getCachedTabs.value.includes(route.name as string);
|
||||
|
||||
// return inTabs && route.meta.loaded ? undefined : transitionName;
|
||||
return transitionName;
|
||||
}
|
||||
const { getEnabledTransition, getTransitionName } = useLayoutHook();
|
||||
|
||||
/**
|
||||
* 转换组件,自动添加 name
|
||||
* @param component
|
||||
* 是否显示component
|
||||
* @param route
|
||||
*/
|
||||
function transformComponent(
|
||||
component: VNode,
|
||||
route: RouteLocationNormalizedLoadedGeneric,
|
||||
) {
|
||||
// 组件视图未找到,如果有设置后备视图,则返回后备视图,如果没有,则抛出错误
|
||||
if (!component) {
|
||||
console.error(
|
||||
'Component view not found,please check the route configuration',
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const routeName = route.name as string;
|
||||
// 如果组件没有 name,则直接返回
|
||||
if (!routeName) {
|
||||
return component;
|
||||
}
|
||||
const componentName = (component?.type as any)?.name;
|
||||
|
||||
// 已经设置过 name,则直接返回
|
||||
if (componentName) {
|
||||
return component;
|
||||
}
|
||||
|
||||
// componentName 与 routeName 一致,则直接返回
|
||||
if (componentName === routeName) {
|
||||
return component;
|
||||
}
|
||||
|
||||
// 设置 name
|
||||
component.type ||= {};
|
||||
(component.type as any).name = routeName;
|
||||
|
||||
return component;
|
||||
}
|
||||
const showComponent = (route: RouteLocationNormalizedLoadedGeneric) => {
|
||||
return !route.meta.domCached && unref(renderRouteView);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative h-full">
|
||||
<IFrameRouterView />
|
||||
<RouteCachedView />
|
||||
<RouterView v-slot="{ Component, route }">
|
||||
<RouteCachedPage
|
||||
:component="Component"
|
||||
:route="route"
|
||||
v-if="route.meta.domCached"
|
||||
/>
|
||||
<Transition
|
||||
v-if="getEnabledTransition"
|
||||
:name="getTransitionName(route)"
|
||||
@@ -113,14 +53,14 @@ function transformComponent(
|
||||
>
|
||||
<component
|
||||
:is="transformComponent(Component, route)"
|
||||
v-if="renderRouteView"
|
||||
v-if="showComponent(route)"
|
||||
v-show="!route.meta.iframeSrc"
|
||||
:key="getTabKey(route)"
|
||||
/>
|
||||
</KeepAlive>
|
||||
<component
|
||||
:is="Component"
|
||||
v-else-if="renderRouteView"
|
||||
v-else-if="showComponent(route)"
|
||||
:key="getTabKey(route)"
|
||||
/>
|
||||
</Transition>
|
||||
@@ -132,14 +72,14 @@ function transformComponent(
|
||||
>
|
||||
<component
|
||||
:is="transformComponent(Component, route)"
|
||||
v-if="renderRouteView"
|
||||
v-if="showComponent(route)"
|
||||
v-show="!route.meta.iframeSrc"
|
||||
:key="getTabKey(route)"
|
||||
/>
|
||||
</KeepAlive>
|
||||
<component
|
||||
:is="Component"
|
||||
v-else-if="renderRouteView"
|
||||
v-else-if="showComponent(route)"
|
||||
:key="getTabKey(route)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -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"
|
||||
@@ -267,6 +268,9 @@ const headerSlots = computed(() => {
|
||||
(value: boolean) =>
|
||||
updatePreferences({ sidebar: { extraCollapse: value } })
|
||||
"
|
||||
@update:sidebar-width="
|
||||
(value: number) => updatePreferences({ sidebar: { width: value } })
|
||||
"
|
||||
>
|
||||
<!-- logo -->
|
||||
<template #logo>
|
||||
|
||||
98
packages/effects/layouts/src/hooks/index.ts
Normal file
98
packages/effects/layouts/src/hooks/index.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import type { VNode } from 'vue';
|
||||
import type {
|
||||
RouteLocationNormalizedLoaded,
|
||||
RouteLocationNormalizedLoadedGeneric,
|
||||
} from 'vue-router';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { preferences, usePreferences } from '@vben/preferences';
|
||||
|
||||
/**
|
||||
* 转换组件,自动添加 name
|
||||
* @param component
|
||||
* @param route
|
||||
*/
|
||||
export function transformComponent(
|
||||
component: VNode,
|
||||
route: RouteLocationNormalizedLoadedGeneric,
|
||||
) {
|
||||
// 组件视图未找到,如果有设置后备视图,则返回后备视图,如果没有,则抛出错误
|
||||
if (!component) {
|
||||
console.error(
|
||||
'Component view not found,please check the route configuration',
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const routeName = route.name as string;
|
||||
// 如果组件没有 name,则直接返回
|
||||
if (!routeName) {
|
||||
return component;
|
||||
}
|
||||
const componentName = (component?.type as any)?.name;
|
||||
|
||||
// 已经设置过 name,则直接返回
|
||||
if (componentName) {
|
||||
return component;
|
||||
}
|
||||
|
||||
// componentName 与 routeName 一致,则直接返回
|
||||
if (componentName === routeName) {
|
||||
return component;
|
||||
}
|
||||
|
||||
// 设置 name
|
||||
component.type ||= {};
|
||||
(component.type as any).name = routeName;
|
||||
|
||||
return component;
|
||||
}
|
||||
|
||||
/**
|
||||
* Layout相关hook
|
||||
*/
|
||||
export function useLayoutHook() {
|
||||
const { keepAlive } = usePreferences();
|
||||
/**
|
||||
* 是否使用动画
|
||||
*/
|
||||
const getEnabledTransition = computed(() => {
|
||||
const { transition } = preferences;
|
||||
const transitionName = transition.name;
|
||||
return transitionName && transition.enable;
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取路由过渡动画
|
||||
* @param _route
|
||||
*/
|
||||
function getTransitionName(_route: RouteLocationNormalizedLoaded) {
|
||||
// 如果偏好设置未设置,则不使用动画
|
||||
const { tabbar, transition } = preferences;
|
||||
const transitionName = transition.name;
|
||||
if (!transitionName || !transition.enable) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 标签页未启用或者未开启缓存,则使用全局配置动画
|
||||
if (!tabbar.enable || !keepAlive) {
|
||||
return transitionName;
|
||||
}
|
||||
|
||||
// 如果页面已经加载过,则不使用动画
|
||||
// if (route.meta.loaded) {
|
||||
// return;
|
||||
// }
|
||||
// 已经打开且已经加载过的页面不使用动画
|
||||
// const inTabs = getCachedTabs.value.includes(route.name as string);
|
||||
|
||||
// return inTabs && route.meta.loaded ? undefined : transitionName;
|
||||
return transitionName;
|
||||
}
|
||||
|
||||
return {
|
||||
getEnabledTransition,
|
||||
getTransitionName,
|
||||
};
|
||||
}
|
||||
2
packages/effects/layouts/src/route-cached/index.ts
Normal file
2
packages/effects/layouts/src/route-cached/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as RouteCachedPage } from './route-cached-page.vue';
|
||||
export { default as RouteCachedView } from './route-cached-view.vue';
|
||||
@@ -0,0 +1,36 @@
|
||||
<!-- 本组件用于获取缓存的route并保存到pinia -->
|
||||
<script setup lang="ts">
|
||||
import type { VNode } from 'vue';
|
||||
import type { RouteLocationNormalizedLoadedGeneric } from 'vue-router';
|
||||
|
||||
import { watch } from 'vue';
|
||||
|
||||
import { useTabbarStore } from '@vben/stores';
|
||||
|
||||
interface Props {
|
||||
component?: VNode;
|
||||
route: RouteLocationNormalizedLoadedGeneric;
|
||||
}
|
||||
|
||||
/**
|
||||
* 这是页面缓存组件,不做任何的的实际渲染
|
||||
*/
|
||||
defineOptions({
|
||||
render() {
|
||||
return null;
|
||||
},
|
||||
});
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const { addCachedRoute } = useTabbarStore();
|
||||
|
||||
watch(
|
||||
() => props.route,
|
||||
() => {
|
||||
if (props.component && props.route.meta.domCached) {
|
||||
addCachedRoute(props.component, props.route);
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
@@ -0,0 +1,98 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, unref, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { getTabKey, storeToRefs, useTabbarStore } from '@vben/stores';
|
||||
|
||||
import { transformComponent, useLayoutHook } from '../hooks';
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const tabbarStore = useTabbarStore();
|
||||
|
||||
const { getTabs, getCachedRoutes, getExcludeCachedTabs } =
|
||||
storeToRefs(tabbarStore);
|
||||
const { removeCachedRoute } = tabbarStore;
|
||||
|
||||
const { getEnabledTransition, getTransitionName } = useLayoutHook();
|
||||
|
||||
/**
|
||||
* 是否启用tab
|
||||
*/
|
||||
const enableTabbar = computed(() => preferences.tabbar.enable);
|
||||
|
||||
const computedCachedRouteKeys = computed(() => {
|
||||
if (!unref(enableTabbar)) {
|
||||
return [];
|
||||
}
|
||||
return unref(getTabs)
|
||||
.filter((item) => item.meta.domCached)
|
||||
.map((item) => getTabKey(item));
|
||||
});
|
||||
|
||||
/**
|
||||
* 监听缓存路由变化,删除不存在的缓存路由
|
||||
*/
|
||||
watch(computedCachedRouteKeys, (keys) => {
|
||||
unref(getCachedRoutes).forEach((item) => {
|
||||
if (!keys.includes(item.key)) {
|
||||
removeCachedRoute(item.key);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 所有缓存的route
|
||||
*/
|
||||
const computedCachedRoutes = computed(() => {
|
||||
if (!unref(enableTabbar)) {
|
||||
return [];
|
||||
}
|
||||
// 刷新路由可刷新缓存
|
||||
const excludeCachedTabKeys = unref(getExcludeCachedTabs);
|
||||
return [...unref(getCachedRoutes).values()].filter((item) => {
|
||||
const componentType: any = item.component.type || {};
|
||||
let componentName = componentType.name;
|
||||
if (!componentName) {
|
||||
componentName = item.route.name;
|
||||
}
|
||||
return !excludeCachedTabKeys.includes(componentName);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 是否显示
|
||||
*/
|
||||
const computedShowView = computed(() => unref(computedCachedRoutes).length > 0);
|
||||
|
||||
const computedCurrentRouteKey = computed(() => {
|
||||
return getTabKey(route);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="computedShowView">
|
||||
<template v-for="item in computedCachedRoutes" :key="item.key">
|
||||
<Transition
|
||||
v-if="getEnabledTransition"
|
||||
appear
|
||||
mode="out-in"
|
||||
:name="getTransitionName(item.route)"
|
||||
>
|
||||
<component
|
||||
v-show="item.key === computedCurrentRouteKey"
|
||||
:is="transformComponent(item.component, item.route)"
|
||||
/>
|
||||
</Transition>
|
||||
<template v-else>
|
||||
<component
|
||||
v-show="item.key === computedCurrentRouteKey"
|
||||
:is="transformComponent(item.component, item.route)"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -15,6 +15,7 @@ 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');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -44,4 +45,7 @@ const appEnableCheckUpdates = defineModel<boolean>('appEnableCheckUpdates');
|
||||
<SwitchItem v-model="appEnableCheckUpdates">
|
||||
{{ $t('preferences.checkUpdates') }}
|
||||
</SwitchItem>
|
||||
<SwitchItem v-model="appEnableCopyPreferences">
|
||||
{{ $t('preferences.enableCopyPreferences') }}
|
||||
</SwitchItem>
|
||||
</template>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -71,6 +71,7 @@ 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 appEnableStickyPreferencesNavigationBar = defineModel<boolean>(
|
||||
'appEnableStickyPreferencesNavigationBar',
|
||||
);
|
||||
@@ -95,6 +96,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',
|
||||
@@ -301,6 +303,7 @@ async function handleReset() {
|
||||
<General
|
||||
v-model:app-dynamic-title="appDynamicTitle"
|
||||
v-model:app-enable-check-updates="appEnableCheckUpdates"
|
||||
v-model:app-enable-copy-preferences="appEnableCopyPreferences"
|
||||
v-model:app-locale="appLocale"
|
||||
v-model:app-watermark="appWatermark"
|
||||
v-model:app-watermark-content="appWatermarkContent"
|
||||
@@ -359,6 +362,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"
|
||||
@@ -470,6 +474,7 @@ async function handleReset() {
|
||||
|
||||
<template #footer>
|
||||
<VbenButton
|
||||
v-if="appEnableCopyPreferences"
|
||||
:disabled="!diffPreference"
|
||||
class="mx-4 w-full"
|
||||
size="sm"
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"plain": "Plain",
|
||||
"rounded": "Rounded",
|
||||
"copyPreferences": "Copy Preferences",
|
||||
"enableCopyPreferences": "Show copy preferences button",
|
||||
"copyPreferencesSuccessTitle": "Copy successful",
|
||||
"copyPreferencesSuccess": "Copy successful, please override in `src/preferences.ts` under app",
|
||||
"clearAndLogout": "Clear Cache & Logout",
|
||||
@@ -54,6 +55,7 @@
|
||||
"title": "Sidebar",
|
||||
"width": "Width",
|
||||
"visible": "Show Sidebar",
|
||||
"draggable": "Drag Sidebar Menu",
|
||||
"collapsed": "Collpase Menu",
|
||||
"collapsedShowTitle": "Show Menu Title",
|
||||
"autoActivateChild": "Auto Activate SubMenu",
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"plain": "朴素",
|
||||
"rounded": "圆润",
|
||||
"copyPreferences": "复制偏好设置",
|
||||
"enableCopyPreferences": "显示复制偏好设置按钮",
|
||||
"copyPreferencesSuccessTitle": "复制成功",
|
||||
"copyPreferencesSuccess": "复制成功,请在 app 下的 `src/preferences.ts`内进行覆盖",
|
||||
"clearAndLogout": "清空缓存 & 退出登录",
|
||||
@@ -54,6 +55,7 @@
|
||||
"title": "侧边栏",
|
||||
"width": "宽度",
|
||||
"visible": "显示侧边栏",
|
||||
"draggable": "侧边栏菜单拖拽",
|
||||
"collapsed": "折叠菜单",
|
||||
"collapsedShowTitle": "折叠显示菜单名",
|
||||
"autoActivateChild": "自动激活子菜单",
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import type { ComputedRef } from 'vue';
|
||||
import type { ComputedRef, VNode } from 'vue';
|
||||
import type {
|
||||
RouteLocationNormalized,
|
||||
RouteLocationNormalizedLoaded,
|
||||
RouteLocationNormalizedLoadedGeneric,
|
||||
Router,
|
||||
RouteRecordNormalized,
|
||||
} from 'vue-router';
|
||||
|
||||
import type { TabDefinition } from '@vben-core/typings';
|
||||
|
||||
import { toRaw } from 'vue';
|
||||
import { markRaw, toRaw } from 'vue';
|
||||
|
||||
import { preferences } from '@vben-core/preferences';
|
||||
import {
|
||||
@@ -20,7 +22,14 @@ import {
|
||||
|
||||
import { acceptHMRUpdate, defineStore } from 'pinia';
|
||||
|
||||
interface RouteCached {
|
||||
component: VNode;
|
||||
key: string;
|
||||
route: RouteLocationNormalizedLoadedGeneric;
|
||||
}
|
||||
|
||||
interface TabbarState {
|
||||
cachedRoutes: Map<string, RouteCached>;
|
||||
/**
|
||||
* @zh_CN 当前打开的标签页列表缓存
|
||||
*/
|
||||
@@ -553,6 +562,25 @@ export const useTabbarStore = defineStore('core-tabbar', {
|
||||
}
|
||||
this.cachedTabs = cacheMap;
|
||||
},
|
||||
/**
|
||||
* 添加缓存的route
|
||||
* @param component
|
||||
* @param route
|
||||
*/
|
||||
addCachedRoute(component: VNode, route: RouteLocationNormalizedLoaded) {
|
||||
const key = getTabKey(route);
|
||||
if (this.cachedRoutes.has(key)) {
|
||||
return;
|
||||
}
|
||||
this.cachedRoutes.set(key, {
|
||||
key,
|
||||
component: markRaw(component),
|
||||
route: markRaw(route),
|
||||
});
|
||||
},
|
||||
removeCachedRoute(key: string) {
|
||||
this.cachedRoutes.delete(key);
|
||||
},
|
||||
},
|
||||
getters: {
|
||||
affixTabs(): TabDefinition[] {
|
||||
@@ -577,6 +605,9 @@ export const useTabbarStore = defineStore('core-tabbar', {
|
||||
const normalTabs = this.tabs.filter((tab) => !isAffixTab(tab));
|
||||
return [...this.affixTabs, ...normalTabs].filter(Boolean);
|
||||
},
|
||||
getCachedRoutes(): Map<string, RouteCached> {
|
||||
return this.cachedRoutes;
|
||||
},
|
||||
},
|
||||
persist: [
|
||||
// tabs不需要保存在localStorage
|
||||
@@ -604,6 +635,7 @@ export const useTabbarStore = defineStore('core-tabbar', {
|
||||
],
|
||||
state: (): TabbarState => ({
|
||||
visitHistory: createStack<string>(true, MAX_VISIT_HISTORY),
|
||||
cachedRoutes: new Map<string, RouteCached>(),
|
||||
cachedTabs: new Set(),
|
||||
dragEndIndex: 0,
|
||||
excludeCachedTabs: new Set(),
|
||||
|
||||
@@ -187,7 +187,7 @@ catalog:
|
||||
vue-eslint-parser: ^10.2.0
|
||||
vue-i18n: ^11.2.8
|
||||
vue-json-viewer: ^3.0.4
|
||||
vue-router: ^4.6.4
|
||||
vue-router: ^5.0.3
|
||||
vue-tippy: ^6.7.1
|
||||
vue-tsc: ^3.2.4
|
||||
vxe-pc-ui: 4.12.16
|
||||
|
||||
Reference in New Issue
Block a user