Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into antdv-next

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

View File

@@ -1,6 +1,6 @@
{
"name": "vben-admin-monorepo",
"version": "5.5.9",
"version": "5.6.0",
"private": true,
"keywords": [
"monorepo",

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>

View File

@@ -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 foundplease 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>

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"
@@ -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>

View 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 foundplease 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,
};
}

View File

@@ -0,0 +1,2 @@
export { default as RouteCachedPage } from './route-cached-page.vue';
export { default as RouteCachedView } from './route-cached-view.vue';

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

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

@@ -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"

View File

@@ -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",

View File

@@ -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": "自动激活子菜单",

View File

@@ -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(),

View File

@@ -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