mirror of
https://gitee.com/dapppp/ruoyi-plus-vben5.git
synced 2026-03-23 04:14:32 +08:00
Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev
This commit is contained in:
@@ -219,6 +219,7 @@ const defaultPreferences: Preferences = {
|
|||||||
dynamicTitle: true,
|
dynamicTitle: true,
|
||||||
enableCheckUpdates: true,
|
enableCheckUpdates: true,
|
||||||
enablePreferences: true,
|
enablePreferences: true,
|
||||||
|
enableCopyPreferences: true,
|
||||||
enableRefreshToken: false,
|
enableRefreshToken: false,
|
||||||
isMobile: false,
|
isMobile: false,
|
||||||
layout: 'sidebar-nav',
|
layout: 'sidebar-nav',
|
||||||
@@ -376,6 +377,8 @@ interface AppPreferences {
|
|||||||
enableCheckUpdates: boolean;
|
enableCheckUpdates: boolean;
|
||||||
/** Whether to display preferences */
|
/** Whether to display preferences */
|
||||||
enablePreferences: boolean;
|
enablePreferences: boolean;
|
||||||
|
/** Whether to display copy preferences button */
|
||||||
|
enableCopyPreferences: boolean;
|
||||||
/**
|
/**
|
||||||
* @zh_CN Whether to enable refreshToken
|
* @zh_CN Whether to enable refreshToken
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -599,6 +599,13 @@ _注意:_ 排序仅针对一级菜单有效,二级菜单的排序需要在对
|
|||||||
|
|
||||||
用于配置当前路由不使用基础布局,仅在顶级时生效。默认情况下,所有的路由都会被包裹在基础布局中(包含顶部以及侧边等导航部件),如果你的页面不需要这些部件,可以设置 `noBasicLayout` 为 `true`。
|
用于配置当前路由不使用基础布局,仅在顶级时生效。默认情况下,所有的路由都会被包裹在基础布局中(包含顶部以及侧边等导航部件),如果你的页面不需要这些部件,可以设置 `noBasicLayout` 为 `true`。
|
||||||
|
|
||||||
|
### domCached
|
||||||
|
|
||||||
|
- 类型:`boolean`
|
||||||
|
- 默认值:`false`
|
||||||
|
|
||||||
|
用于配置当前路由是否要将route对应dom元素缓存起来。对于一些复杂页面切换tab浏览器回流/重绘会导致卡顿, `domCached` 设为 `true`可解决该问题,但是也有代价:1、内存占用升高 2、vue的部分生命周期不会触发
|
||||||
|
|
||||||
## 路由刷新
|
## 路由刷新
|
||||||
|
|
||||||
路由刷新方式如下:
|
路由刷新方式如下:
|
||||||
|
|||||||
@@ -218,6 +218,7 @@ const defaultPreferences: Preferences = {
|
|||||||
dynamicTitle: true,
|
dynamicTitle: true,
|
||||||
enableCheckUpdates: true,
|
enableCheckUpdates: true,
|
||||||
enablePreferences: true,
|
enablePreferences: true,
|
||||||
|
enableCopyPreferences: true,
|
||||||
enableRefreshToken: false,
|
enableRefreshToken: false,
|
||||||
isMobile: false,
|
isMobile: false,
|
||||||
layout: 'sidebar-nav',
|
layout: 'sidebar-nav',
|
||||||
@@ -375,6 +376,8 @@ interface AppPreferences {
|
|||||||
enableCheckUpdates: boolean;
|
enableCheckUpdates: boolean;
|
||||||
/** 是否显示偏好设置 */
|
/** 是否显示偏好设置 */
|
||||||
enablePreferences: boolean;
|
enablePreferences: boolean;
|
||||||
|
/** 是否显示复制偏好设置按钮 */
|
||||||
|
enableCopyPreferences: boolean;
|
||||||
/**
|
/**
|
||||||
* @zh_CN 是否开启refreshToken
|
* @zh_CN 是否开启refreshToken
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "vben-admin-monorepo",
|
"name": "vben-admin-monorepo",
|
||||||
"version": "5.5.9",
|
"version": "5.6.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"monorepo",
|
"monorepo",
|
||||||
|
|||||||
@@ -43,6 +43,10 @@ interface RouteMeta {
|
|||||||
| 'success'
|
| 'success'
|
||||||
| 'warning'
|
| 'warning'
|
||||||
| string;
|
| string;
|
||||||
|
/**
|
||||||
|
* 路由对应dom是否缓存起来
|
||||||
|
*/
|
||||||
|
domCached?: boolean;
|
||||||
/**
|
/**
|
||||||
* 路由的完整路径作为key(默认true)
|
* 路由的完整路径作为key(默认true)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ exports[`defaultPreferences immutability test > should not modify the config obj
|
|||||||
"dynamicTitle": true,
|
"dynamicTitle": true,
|
||||||
"enableCheckUpdates": true,
|
"enableCheckUpdates": true,
|
||||||
"enablePreferences": true,
|
"enablePreferences": true,
|
||||||
|
"enableCopyPreferences": true,
|
||||||
"enableRefreshToken": false,
|
"enableRefreshToken": false,
|
||||||
"enableStickyPreferencesNavigationBar": true,
|
"enableStickyPreferencesNavigationBar": true,
|
||||||
"isMobile": false,
|
"isMobile": false,
|
||||||
@@ -84,6 +85,7 @@ exports[`defaultPreferences immutability test > should not modify the config obj
|
|||||||
"collapsed": false,
|
"collapsed": false,
|
||||||
"collapsedButton": true,
|
"collapsedButton": true,
|
||||||
"collapsedShowTitle": false,
|
"collapsedShowTitle": false,
|
||||||
|
"draggable": true,
|
||||||
"enable": true,
|
"enable": true,
|
||||||
"expandOnHover": true,
|
"expandOnHover": true,
|
||||||
"extraCollapse": false,
|
"extraCollapse": false,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const defaultPreferences: Preferences = {
|
|||||||
dynamicTitle: true,
|
dynamicTitle: true,
|
||||||
enableCheckUpdates: true,
|
enableCheckUpdates: true,
|
||||||
enablePreferences: true,
|
enablePreferences: true,
|
||||||
|
enableCopyPreferences: true,
|
||||||
enableRefreshToken: false,
|
enableRefreshToken: false,
|
||||||
enableStickyPreferencesNavigationBar: true,
|
enableStickyPreferencesNavigationBar: true,
|
||||||
isMobile: false,
|
isMobile: false,
|
||||||
@@ -85,6 +86,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,
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ interface AppPreferences {
|
|||||||
enableCheckUpdates: boolean;
|
enableCheckUpdates: boolean;
|
||||||
/** 是否显示偏好设置 */
|
/** 是否显示偏好设置 */
|
||||||
enablePreferences: boolean;
|
enablePreferences: boolean;
|
||||||
|
/** 是否显示复制偏好设置按钮 */
|
||||||
|
enableCopyPreferences: boolean;
|
||||||
/**
|
/**
|
||||||
* @zh_CN 是否开启refreshToken
|
* @zh_CN 是否开启refreshToken
|
||||||
*/
|
*/
|
||||||
@@ -170,6 +172,8 @@ interface SidebarPreferences {
|
|||||||
collapsedShowTitle: boolean;
|
collapsedShowTitle: boolean;
|
||||||
/** 侧边栏折叠宽度 */
|
/** 侧边栏折叠宽度 */
|
||||||
collapseWidth: number;
|
collapseWidth: number;
|
||||||
|
/** 侧边栏菜单拖拽 */
|
||||||
|
draggable: boolean;
|
||||||
/** 侧边栏是否可见 */
|
/** 侧边栏是否可见 */
|
||||||
enable: boolean;
|
enable: boolean;
|
||||||
/** 菜单自动展开状态 */
|
/** 菜单自动展开状态 */
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { VbenScrollbar } from '@vben-core/shadcn-ui';
|
|||||||
|
|
||||||
import { useScrollLock } from '@vueuse/core';
|
import { useScrollLock } from '@vueuse/core';
|
||||||
|
|
||||||
|
import { useSidebarDrag } from '../hooks/use-sidebar-drag';
|
||||||
import { SidebarCollapseButton, SidebarFixedButton } from './widgets';
|
import { SidebarCollapseButton, SidebarFixedButton } from './widgets';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -107,7 +108,8 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
zIndex: 0,
|
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 collapse = defineModel<boolean>('collapse');
|
||||||
const extraCollapse = defineModel<boolean>('extraCollapse');
|
const extraCollapse = defineModel<boolean>('extraCollapse');
|
||||||
const expandOnHovering = defineModel<boolean>('expandOnHovering');
|
const expandOnHovering = defineModel<boolean>('expandOnHovering');
|
||||||
@@ -117,8 +119,8 @@ const extraVisible = defineModel<boolean>('extraVisible');
|
|||||||
const isLocked = useScrollLock(document.body);
|
const isLocked = useScrollLock(document.body);
|
||||||
const slots = useSlots();
|
const slots = useSlots();
|
||||||
|
|
||||||
// @ts-expect-error unused
|
const asideRef = shallowRef<HTMLElement | null>(null);
|
||||||
const asideRef = shallowRef<HTMLDivElement | null>();
|
const dragBarRef = shallowRef<HTMLElement | null>(null);
|
||||||
|
|
||||||
const hiddenSideStyle = computed((): CSSProperties => calcMenuWidthStyle(true));
|
const hiddenSideStyle = computed((): CSSProperties => calcMenuWidthStyle(true));
|
||||||
|
|
||||||
@@ -155,9 +157,9 @@ const extraTitleStyle = computed((): CSSProperties => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const contentWidthStyle = computed((): CSSProperties => {
|
const contentWidthStyle = computed((): CSSProperties => {
|
||||||
const { collapseWidth, fixedExtra, isSidebarMixed, mixedWidth } = props;
|
const { fixedExtra, isSidebarMixed, mixedWidth } = props;
|
||||||
if (isSidebarMixed && fixedExtra) {
|
if (isSidebarMixed && fixedExtra) {
|
||||||
return { width: `${collapse.value ? collapseWidth : mixedWidth}px` };
|
return { width: `${mixedWidth}px` };
|
||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
});
|
});
|
||||||
@@ -200,19 +202,24 @@ watchEffect(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function calcMenuWidthStyle(isHiddenDom: boolean): CSSProperties {
|
function calcMenuWidthStyle(isHiddenDom: boolean): CSSProperties {
|
||||||
const { extraWidth, fixedExtra, isSidebarMixed, show, width } = props;
|
const {
|
||||||
|
collapseWidth,
|
||||||
|
extraWidth,
|
||||||
|
mixedWidth,
|
||||||
|
fixedExtra,
|
||||||
|
isSidebarMixed,
|
||||||
|
show,
|
||||||
|
width,
|
||||||
|
} = props;
|
||||||
|
|
||||||
let widthValue =
|
let widthValue =
|
||||||
width === 0
|
width === 0
|
||||||
? '0px'
|
? '0px'
|
||||||
: `${width + (isSidebarMixed && fixedExtra && extraVisible.value ? extraWidth : 0)}px`;
|
: `${width + (isSidebarMixed && fixedExtra && extraVisible.value ? extraWidth : 0)}px`;
|
||||||
|
|
||||||
const { collapseWidth } = props;
|
|
||||||
|
|
||||||
if (isHiddenDom && expandOnHovering.value && !expandOnHover.value) {
|
if (isHiddenDom && expandOnHovering.value && !expandOnHover.value) {
|
||||||
widthValue = `${collapseWidth}px`;
|
widthValue = isSidebarMixed ? `${mixedWidth}px` : `${collapseWidth}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...(widthValue === '0px' ? { overflow: 'hidden' } : {}),
|
...(widthValue === '0px' ? { overflow: 'hidden' } : {}),
|
||||||
flex: `0 0 ${widthValue}`,
|
flex: `0 0 ${widthValue}`,
|
||||||
@@ -254,6 +261,38 @@ function handleMouseleave() {
|
|||||||
collapse.value = true;
|
collapse.value = true;
|
||||||
extraVisible.value = false;
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -264,6 +303,7 @@ function handleMouseleave() {
|
|||||||
class="h-full transition-all duration-150"
|
class="h-full transition-all duration-150"
|
||||||
></div>
|
></div>
|
||||||
<aside
|
<aside
|
||||||
|
ref="asideRef"
|
||||||
:style="style"
|
:style="style"
|
||||||
class="fixed left-0 top-0 h-full transition-all duration-150"
|
class="fixed left-0 top-0 h-full transition-all duration-150"
|
||||||
@mouseenter="handleMouseenter"
|
@mouseenter="handleMouseenter"
|
||||||
@@ -299,7 +339,6 @@ function handleMouseleave() {
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="isSidebarMixed"
|
v-if="isSidebarMixed"
|
||||||
ref="asideRef"
|
|
||||||
:class="[
|
:class="[
|
||||||
themeSub,
|
themeSub,
|
||||||
{
|
{
|
||||||
@@ -330,5 +369,11 @@ function handleMouseleave() {
|
|||||||
<slot name="extra"></slot>
|
<slot name="extra"></slot>
|
||||||
</VbenScrollbar>
|
</VbenScrollbar>
|
||||||
</div>
|
</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>
|
</aside>
|
||||||
</template>
|
</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,
|
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', {
|
const sidebarCollapse = defineModel<boolean>('sidebarCollapse', {
|
||||||
default: false,
|
default: false,
|
||||||
});
|
});
|
||||||
@@ -120,13 +127,16 @@ const headerWrapperHeight = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const getSideCollapseWidth = computed(() => {
|
const getSideCollapseWidth = computed(() => {
|
||||||
const { sidebarCollapseShowTitle, sidebarMixedWidth, sideCollapseWidth } =
|
const {
|
||||||
props;
|
sidebarCollapseShowTitle,
|
||||||
|
sidebarExtraCollapsedWidth,
|
||||||
|
sideCollapseWidth,
|
||||||
|
} = props;
|
||||||
|
|
||||||
return sidebarCollapseShowTitle ||
|
return sidebarCollapseShowTitle ||
|
||||||
isSidebarMixedNav.value ||
|
isSidebarMixedNav.value ||
|
||||||
isHeaderMixedNav.value
|
isHeaderMixedNav.value
|
||||||
? sidebarMixedWidth
|
? sidebarExtraCollapsedWidth
|
||||||
: sideCollapseWidth;
|
: sideCollapseWidth;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -237,9 +247,7 @@ const mainStyle = computed(() => {
|
|||||||
sidebarExtraVisible.value;
|
sidebarExtraVisible.value;
|
||||||
|
|
||||||
if (isSideNavEffective) {
|
if (isSideNavEffective) {
|
||||||
const sideCollapseWidth = sidebarCollapse.value
|
const sideCollapseWidth = props.sidebarMixedWidth;
|
||||||
? getSideCollapseWidth.value
|
|
||||||
: props.sidebarMixedWidth;
|
|
||||||
const sideWidth = sidebarExtraCollapse.value
|
const sideWidth = sidebarExtraCollapse.value
|
||||||
? props.sidebarExtraCollapsedWidth
|
? props.sidebarExtraCollapsedWidth
|
||||||
: props.sidebarWidth;
|
: props.sidebarWidth;
|
||||||
@@ -248,10 +256,14 @@ const mainStyle = computed(() => {
|
|||||||
sidebarAndExtraWidth = `${sideCollapseWidth + sideWidth}px`;
|
sidebarAndExtraWidth = `${sideCollapseWidth + sideWidth}px`;
|
||||||
width = `calc(100% - ${sidebarAndExtraWidth})`;
|
width = `calc(100% - ${sidebarAndExtraWidth})`;
|
||||||
} else {
|
} else {
|
||||||
sidebarAndExtraWidth =
|
let sidebarWidth = getSidebarWidth.value;
|
||||||
sidebarExpandOnHovering.value && !sidebarExpandOnHover.value
|
if (sidebarExpandOnHovering.value && !sidebarExpandOnHover.value) {
|
||||||
? `${getSideCollapseWidth.value}px`
|
sidebarWidth =
|
||||||
: `${getSidebarWidth.value}px`;
|
isSidebarMixedNav.value || isHeaderMixedNav.value
|
||||||
|
? props.sidebarMixedWidth
|
||||||
|
: getSideCollapseWidth.value;
|
||||||
|
}
|
||||||
|
sidebarAndExtraWidth = `${sidebarWidth}px`;
|
||||||
width = `calc(100% - ${sidebarAndExtraWidth})`;
|
width = `calc(100% - ${sidebarAndExtraWidth})`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -486,6 +498,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"
|
||||||
@@ -507,6 +520,7 @@ const idMainContent = ELEMENT_ID_MAIN_CONTENT;
|
|||||||
:width="getSidebarWidth"
|
:width="getSidebarWidth"
|
||||||
:z-index="sidebarZIndex"
|
:z-index="sidebarZIndex"
|
||||||
@leave="() => emit('sideMouseLeave')"
|
@leave="() => emit('sideMouseLeave')"
|
||||||
|
@update:width="(val) => emit('update:sidebar-width', val)"
|
||||||
>
|
>
|
||||||
<template v-if="isSideMode && !isMixedNav" #logo>
|
<template v-if="isSideMode && !isMixedNav" #logo>
|
||||||
<slot name="logo"></slot>
|
<slot name="logo"></slot>
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { VNode } from 'vue';
|
import type { RouteLocationNormalizedLoadedGeneric } from 'vue-router';
|
||||||
import type {
|
|
||||||
RouteLocationNormalizedLoaded,
|
|
||||||
RouteLocationNormalizedLoadedGeneric,
|
|
||||||
} from 'vue-router';
|
|
||||||
|
|
||||||
import { computed } from 'vue';
|
import { unref } from 'vue';
|
||||||
import { RouterView } from 'vue-router';
|
import { RouterView } from 'vue-router';
|
||||||
|
|
||||||
import { preferences, usePreferences } from '@vben/preferences';
|
import { usePreferences } from '@vben/preferences';
|
||||||
import { getTabKey, storeToRefs, useTabbarStore } from '@vben/stores';
|
import { getTabKey, storeToRefs, useTabbarStore } from '@vben/stores';
|
||||||
|
|
||||||
|
import { transformComponent, useLayoutHook } from '../../hooks';
|
||||||
import { IFrameRouterView } from '../../iframe';
|
import { IFrameRouterView } from '../../iframe';
|
||||||
|
import { RouteCachedPage, RouteCachedView } from '../../route-cached';
|
||||||
|
|
||||||
defineOptions({ name: 'LayoutContent' });
|
defineOptions({ name: 'LayoutContent' });
|
||||||
|
|
||||||
@@ -21,85 +19,27 @@ const { keepAlive } = usePreferences();
|
|||||||
const { getCachedTabs, getExcludeCachedTabs, renderRouteView } =
|
const { getCachedTabs, getExcludeCachedTabs, renderRouteView } =
|
||||||
storeToRefs(tabbarStore);
|
storeToRefs(tabbarStore);
|
||||||
|
|
||||||
/**
|
const { getEnabledTransition, getTransitionName } = useLayoutHook();
|
||||||
* 是否使用动画
|
|
||||||
*/
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 转换组件,自动添加 name
|
* 是否显示component
|
||||||
* @param component
|
* @param route
|
||||||
*/
|
*/
|
||||||
function transformComponent(
|
const showComponent = (route: RouteLocationNormalizedLoadedGeneric) => {
|
||||||
component: VNode,
|
return !route.meta.domCached && unref(renderRouteView);
|
||||||
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;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="relative h-full">
|
<div class="relative h-full">
|
||||||
<IFrameRouterView />
|
<IFrameRouterView />
|
||||||
|
<RouteCachedView />
|
||||||
<RouterView v-slot="{ Component, route }">
|
<RouterView v-slot="{ Component, route }">
|
||||||
|
<RouteCachedPage
|
||||||
|
:component="Component"
|
||||||
|
:route="route"
|
||||||
|
v-if="route.meta.domCached"
|
||||||
|
/>
|
||||||
<Transition
|
<Transition
|
||||||
v-if="getEnabledTransition"
|
v-if="getEnabledTransition"
|
||||||
:name="getTransitionName(route)"
|
:name="getTransitionName(route)"
|
||||||
@@ -113,14 +53,14 @@ function transformComponent(
|
|||||||
>
|
>
|
||||||
<component
|
<component
|
||||||
:is="transformComponent(Component, route)"
|
:is="transformComponent(Component, route)"
|
||||||
v-if="renderRouteView"
|
v-if="showComponent(route)"
|
||||||
v-show="!route.meta.iframeSrc"
|
v-show="!route.meta.iframeSrc"
|
||||||
:key="getTabKey(route)"
|
:key="getTabKey(route)"
|
||||||
/>
|
/>
|
||||||
</KeepAlive>
|
</KeepAlive>
|
||||||
<component
|
<component
|
||||||
:is="Component"
|
:is="Component"
|
||||||
v-else-if="renderRouteView"
|
v-else-if="showComponent(route)"
|
||||||
:key="getTabKey(route)"
|
:key="getTabKey(route)"
|
||||||
/>
|
/>
|
||||||
</Transition>
|
</Transition>
|
||||||
@@ -132,14 +72,14 @@ function transformComponent(
|
|||||||
>
|
>
|
||||||
<component
|
<component
|
||||||
:is="transformComponent(Component, route)"
|
:is="transformComponent(Component, route)"
|
||||||
v-if="renderRouteView"
|
v-if="showComponent(route)"
|
||||||
v-show="!route.meta.iframeSrc"
|
v-show="!route.meta.iframeSrc"
|
||||||
:key="getTabKey(route)"
|
:key="getTabKey(route)"
|
||||||
/>
|
/>
|
||||||
</KeepAlive>
|
</KeepAlive>
|
||||||
<component
|
<component
|
||||||
:is="Component"
|
:is="Component"
|
||||||
v-else-if="renderRouteView"
|
v-else-if="showComponent(route)"
|
||||||
:key="getTabKey(route)"
|
:key="getTabKey(route)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -267,6 +268,9 @@ const headerSlots = computed(() => {
|
|||||||
(value: boolean) =>
|
(value: boolean) =>
|
||||||
updatePreferences({ sidebar: { extraCollapse: value } })
|
updatePreferences({ sidebar: { extraCollapse: value } })
|
||||||
"
|
"
|
||||||
|
@update:sidebar-width="
|
||||||
|
(value: number) => updatePreferences({ sidebar: { width: value } })
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<!-- logo -->
|
<!-- logo -->
|
||||||
<template #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 appWatermark = defineModel<boolean>('appWatermark');
|
||||||
const appWatermarkContent = defineModel<string>('appWatermarkContent');
|
const appWatermarkContent = defineModel<string>('appWatermarkContent');
|
||||||
const appEnableCheckUpdates = defineModel<boolean>('appEnableCheckUpdates');
|
const appEnableCheckUpdates = defineModel<boolean>('appEnableCheckUpdates');
|
||||||
|
const appEnableCopyPreferences = defineModel<boolean>('appEnableCopyPreferences');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -44,4 +45,7 @@ const appEnableCheckUpdates = defineModel<boolean>('appEnableCheckUpdates');
|
|||||||
<SwitchItem v-model="appEnableCheckUpdates">
|
<SwitchItem v-model="appEnableCheckUpdates">
|
||||||
{{ $t('preferences.checkUpdates') }}
|
{{ $t('preferences.checkUpdates') }}
|
||||||
</SwitchItem>
|
</SwitchItem>
|
||||||
|
<SwitchItem v-model="appEnableCopyPreferences">
|
||||||
|
{{ $t('preferences.enableCopyPreferences') }}
|
||||||
|
</SwitchItem>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ const appContentCompact = defineModel<ContentCompactType>('appContentCompact');
|
|||||||
const appWatermark = defineModel<boolean>('appWatermark');
|
const appWatermark = defineModel<boolean>('appWatermark');
|
||||||
const appWatermarkContent = defineModel<string>('appWatermarkContent');
|
const appWatermarkContent = defineModel<string>('appWatermarkContent');
|
||||||
const appEnableCheckUpdates = defineModel<boolean>('appEnableCheckUpdates');
|
const appEnableCheckUpdates = defineModel<boolean>('appEnableCheckUpdates');
|
||||||
|
const appEnableCopyPreferences = defineModel<boolean>('appEnableCopyPreferences');
|
||||||
const appEnableStickyPreferencesNavigationBar = defineModel<boolean>(
|
const appEnableStickyPreferencesNavigationBar = defineModel<boolean>(
|
||||||
'appEnableStickyPreferencesNavigationBar',
|
'appEnableStickyPreferencesNavigationBar',
|
||||||
);
|
);
|
||||||
@@ -93,6 +94,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',
|
||||||
@@ -299,6 +301,7 @@ async function handleReset() {
|
|||||||
<General
|
<General
|
||||||
v-model:app-dynamic-title="appDynamicTitle"
|
v-model:app-dynamic-title="appDynamicTitle"
|
||||||
v-model:app-enable-check-updates="appEnableCheckUpdates"
|
v-model:app-enable-check-updates="appEnableCheckUpdates"
|
||||||
|
v-model:app-enable-copy-preferences="appEnableCopyPreferences"
|
||||||
v-model:app-locale="appLocale"
|
v-model:app-locale="appLocale"
|
||||||
v-model:app-watermark="appWatermark"
|
v-model:app-watermark="appWatermark"
|
||||||
v-model:app-watermark-content="appWatermarkContent"
|
v-model:app-watermark-content="appWatermarkContent"
|
||||||
@@ -354,6 +357,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"
|
||||||
@@ -465,6 +469,7 @@ async function handleReset() {
|
|||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<VbenButton
|
<VbenButton
|
||||||
|
v-if="appEnableCopyPreferences"
|
||||||
:disabled="!diffPreference"
|
:disabled="!diffPreference"
|
||||||
class="mx-4 w-full"
|
class="mx-4 w-full"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
"plain": "Plain",
|
"plain": "Plain",
|
||||||
"rounded": "Rounded",
|
"rounded": "Rounded",
|
||||||
"copyPreferences": "Copy Preferences",
|
"copyPreferences": "Copy Preferences",
|
||||||
|
"enableCopyPreferences": "Show copy preferences button",
|
||||||
"copyPreferencesSuccessTitle": "Copy successful",
|
"copyPreferencesSuccessTitle": "Copy successful",
|
||||||
"copyPreferencesSuccess": "Copy successful, please override in `src/preferences.ts` under app",
|
"copyPreferencesSuccess": "Copy successful, please override in `src/preferences.ts` under app",
|
||||||
"clearAndLogout": "Clear Cache & Logout",
|
"clearAndLogout": "Clear Cache & Logout",
|
||||||
@@ -54,6 +55,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",
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
"plain": "朴素",
|
"plain": "朴素",
|
||||||
"rounded": "圆润",
|
"rounded": "圆润",
|
||||||
"copyPreferences": "复制偏好设置",
|
"copyPreferences": "复制偏好设置",
|
||||||
|
"enableCopyPreferences": "显示复制偏好设置按钮",
|
||||||
"copyPreferencesSuccessTitle": "复制成功",
|
"copyPreferencesSuccessTitle": "复制成功",
|
||||||
"copyPreferencesSuccess": "复制成功,请在 app 下的 `src/preferences.ts`内进行覆盖",
|
"copyPreferencesSuccess": "复制成功,请在 app 下的 `src/preferences.ts`内进行覆盖",
|
||||||
"clearAndLogout": "清空缓存 & 退出登录",
|
"clearAndLogout": "清空缓存 & 退出登录",
|
||||||
@@ -54,6 +55,7 @@
|
|||||||
"title": "侧边栏",
|
"title": "侧边栏",
|
||||||
"width": "宽度",
|
"width": "宽度",
|
||||||
"visible": "显示侧边栏",
|
"visible": "显示侧边栏",
|
||||||
|
"draggable": "侧边栏菜单拖拽",
|
||||||
"collapsed": "折叠菜单",
|
"collapsed": "折叠菜单",
|
||||||
"collapsedShowTitle": "折叠显示菜单名",
|
"collapsedShowTitle": "折叠显示菜单名",
|
||||||
"autoActivateChild": "自动激活子菜单",
|
"autoActivateChild": "自动激活子菜单",
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import type { ComputedRef } from 'vue';
|
import type { ComputedRef, VNode } from 'vue';
|
||||||
import type {
|
import type {
|
||||||
RouteLocationNormalized,
|
RouteLocationNormalized,
|
||||||
|
RouteLocationNormalizedLoaded,
|
||||||
|
RouteLocationNormalizedLoadedGeneric,
|
||||||
Router,
|
Router,
|
||||||
RouteRecordNormalized,
|
RouteRecordNormalized,
|
||||||
} from 'vue-router';
|
} from 'vue-router';
|
||||||
|
|
||||||
import type { TabDefinition } from '@vben-core/typings';
|
import type { TabDefinition } from '@vben-core/typings';
|
||||||
|
|
||||||
import { toRaw } from 'vue';
|
import { markRaw, toRaw } from 'vue';
|
||||||
|
|
||||||
import { preferences } from '@vben-core/preferences';
|
import { preferences } from '@vben-core/preferences';
|
||||||
import {
|
import {
|
||||||
@@ -20,7 +22,14 @@ import {
|
|||||||
|
|
||||||
import { acceptHMRUpdate, defineStore } from 'pinia';
|
import { acceptHMRUpdate, defineStore } from 'pinia';
|
||||||
|
|
||||||
|
interface RouteCached {
|
||||||
|
component: VNode;
|
||||||
|
key: string;
|
||||||
|
route: RouteLocationNormalizedLoadedGeneric;
|
||||||
|
}
|
||||||
|
|
||||||
interface TabbarState {
|
interface TabbarState {
|
||||||
|
cachedRoutes: Map<string, RouteCached>;
|
||||||
/**
|
/**
|
||||||
* @zh_CN 当前打开的标签页列表缓存
|
* @zh_CN 当前打开的标签页列表缓存
|
||||||
*/
|
*/
|
||||||
@@ -553,6 +562,25 @@ export const useTabbarStore = defineStore('core-tabbar', {
|
|||||||
}
|
}
|
||||||
this.cachedTabs = cacheMap;
|
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: {
|
getters: {
|
||||||
affixTabs(): TabDefinition[] {
|
affixTabs(): TabDefinition[] {
|
||||||
@@ -577,6 +605,9 @@ export const useTabbarStore = defineStore('core-tabbar', {
|
|||||||
const normalTabs = this.tabs.filter((tab) => !isAffixTab(tab));
|
const normalTabs = this.tabs.filter((tab) => !isAffixTab(tab));
|
||||||
return [...this.affixTabs, ...normalTabs].filter(Boolean);
|
return [...this.affixTabs, ...normalTabs].filter(Boolean);
|
||||||
},
|
},
|
||||||
|
getCachedRoutes(): Map<string, RouteCached> {
|
||||||
|
return this.cachedRoutes;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
persist: [
|
persist: [
|
||||||
// tabs不需要保存在localStorage
|
// tabs不需要保存在localStorage
|
||||||
@@ -604,6 +635,7 @@ export const useTabbarStore = defineStore('core-tabbar', {
|
|||||||
],
|
],
|
||||||
state: (): TabbarState => ({
|
state: (): TabbarState => ({
|
||||||
visitHistory: createStack<string>(true, MAX_VISIT_HISTORY),
|
visitHistory: createStack<string>(true, MAX_VISIT_HISTORY),
|
||||||
|
cachedRoutes: new Map<string, RouteCached>(),
|
||||||
cachedTabs: new Set(),
|
cachedTabs: new Set(),
|
||||||
dragEndIndex: 0,
|
dragEndIndex: 0,
|
||||||
excludeCachedTabs: new Set(),
|
excludeCachedTabs: new Set(),
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
meta: {
|
meta: {
|
||||||
icon: 'mdi:page-previous-outline',
|
icon: 'mdi:page-previous-outline',
|
||||||
title: $t('demos.access.pageAccess'),
|
title: $t('demos.access.pageAccess'),
|
||||||
|
domCached: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ catalog:
|
|||||||
vue-eslint-parser: ^10.2.0
|
vue-eslint-parser: ^10.2.0
|
||||||
vue-i18n: ^11.2.8
|
vue-i18n: ^11.2.8
|
||||||
vue-json-viewer: ^3.0.4
|
vue-json-viewer: ^3.0.4
|
||||||
vue-router: ^4.6.4
|
vue-router: ^5.0.3
|
||||||
vue-tippy: ^6.7.1
|
vue-tippy: ^6.7.1
|
||||||
vue-tsc: ^3.2.4
|
vue-tsc: ^3.2.4
|
||||||
vue3-colorpicker: ^2.3.0
|
vue3-colorpicker: ^2.3.0
|
||||||
|
|||||||
Reference in New Issue
Block a user