This commit is contained in:
dap 2024-12-31 12:31:49 +08:00
commit cf9edbb1c4
21 changed files with 400 additions and 24 deletions

View File

@ -54,6 +54,7 @@ Drawer 内的内容一般业务中,会比较复杂,所以我们可以将 dra
- `VbenDrawer` 组件对与参数的处理优先级是 `slot` > `props` > `state`(通过api更新的状态以及useVbenDrawer参数)。如果你已经传入了 `slot` 或者 `props`,那么 `setState` 将不会生效,这种情况下你可以通过 `slot` 或者 `props` 来更新状态。 - `VbenDrawer` 组件对与参数的处理优先级是 `slot` > `props` > `state`(通过api更新的状态以及useVbenDrawer参数)。如果你已经传入了 `slot` 或者 `props`,那么 `setState` 将不会生效,这种情况下你可以通过 `slot` 或者 `props` 来更新状态。
- 如果你使用到了 `connectedComponent` 参数,那么会存在 2 个`useVbenDrawer`, 此时,如果同时设置了相同的参数,那么以内部为准(也就是没有设置 connectedComponent 的代码)。比如 同时设置了 `onConfirm`,那么以内部的 `onConfirm` 为准。`onOpenChange`事件除外,内外都会触发。 - 如果你使用到了 `connectedComponent` 参数,那么会存在 2 个`useVbenDrawer`, 此时,如果同时设置了相同的参数,那么以内部为准(也就是没有设置 connectedComponent 的代码)。比如 同时设置了 `onConfirm`,那么以内部的 `onConfirm` 为准。`onOpenChange`事件除外,内外都会触发。
- 使用了`connectedComponent`参数时,可以配置`destroyOnClose`属性来决定当关闭弹窗时,是否要销毁`connectedComponent`组件(重新创建`connectedComponent`组件,这将会把其内部所有的变量、状态、数据等恢复到初始状态。)。
::: :::
@ -75,12 +76,15 @@ const [Drawer, drawerApi] = useVbenDrawer({
| 属性名 | 描述 | 类型 | 默认值 | | 属性名 | 描述 | 类型 | 默认值 |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| appendToMain | 是否挂载到内容区域默认挂载到body | `boolean` | `false` | | appendToMain | 是否挂载到内容区域默认挂载到body | `boolean` | `false` |
| connectedComponent | 连接另一个Modal组件 | `Component` | - |
| destroyOnClose | 关闭时销毁`connectedComponent` | `boolean` | `false` |
| title | 标题 | `string\|slot` | - | | title | 标题 | `string\|slot` | - |
| titleTooltip | 标题提示信息 | `string\|slot` | - | | titleTooltip | 标题提示信息 | `string\|slot` | - |
| description | 描述信息 | `string\|slot` | - | | description | 描述信息 | `string\|slot` | - |
| isOpen | 弹窗打开状态 | `boolean` | `false` | | isOpen | 弹窗打开状态 | `boolean` | `false` |
| loading | 弹窗加载状态 | `boolean` | `false` | | loading | 弹窗加载状态 | `boolean` | `false` |
| closable | 显示关闭按钮 | `boolean` | `true` | | closable | 显示关闭按钮 | `boolean` | `true` |
| closeIconPlacement | 关闭按钮位置 | `'left'\|'right'` | `right` |
| modal | 显示遮罩 | `boolean` | `true` | | modal | 显示遮罩 | `boolean` | `true` |
| header | 显示header | `boolean` | `true` | | header | 显示header | `boolean` | `true` |
| footer | 显示footer | `boolean\|slot` | `true` | | footer | 显示footer | `boolean\|slot` | `true` |
@ -108,12 +112,14 @@ const [Drawer, drawerApi] = useVbenDrawer({
以下事件,只有在 `useVbenDrawer({onCancel:()=>{}})` 中传入才会生效。 以下事件,只有在 `useVbenDrawer({onCancel:()=>{}})` 中传入才会生效。
| 事件名 | 描述 | 类型 | | 事件名 | 描述 | 类型 | 版本限制 |
| --- | --- | --- | | --- | --- | --- | --- |
| onBeforeClose | 关闭前触发,返回 `false`则禁止关闭 | `()=>boolean` | | onBeforeClose | 关闭前触发,返回 `false`则禁止关闭 | `()=>boolean` | --- |
| onCancel | 点击取消按钮触发 | `()=>void` | | onCancel | 点击取消按钮触发 | `()=>void` | --- |
| onConfirm | 点击确认按钮触发 | `()=>void` | | onClosed | 关闭动画播放完毕时触发 | `()=>void` | >5.5.2 |
| onOpenChange | 关闭或者打开弹窗时触发 | `(isOpen:boolean)=>void` | | onConfirm | 点击确认按钮触发 | `()=>void` | --- |
| onOpenChange | 关闭或者打开弹窗时触发 | `(isOpen:boolean)=>void` | --- |
| onOpened | 打开动画播放完毕时触发 | `()=>void` | >5.5.2 |
### Slots ### Slots
@ -124,6 +130,8 @@ const [Drawer, drawerApi] = useVbenDrawer({
| default | 默认插槽 - 弹窗内容 | | default | 默认插槽 - 弹窗内容 |
| prepend-footer | 取消按钮左侧 | | prepend-footer | 取消按钮左侧 |
| append-footer | 取消按钮右侧 | | append-footer | 取消按钮右侧 |
| close-icon | 关闭按钮图标 |
| extra | 额外内容(标题右侧) |
### modalApi ### modalApi

View File

@ -60,6 +60,7 @@ Modal 内的内容一般业务中,会比较复杂,所以我们可以将 moda
- `VbenModal` 组件对与参数的处理优先级是 `slot` > `props` > `state`(通过api更新的状态以及useVbenModal参数)。如果你已经传入了 `slot` 或者 `props`,那么 `setState` 将不会生效,这种情况下你可以通过 `slot` 或者 `props` 来更新状态。 - `VbenModal` 组件对与参数的处理优先级是 `slot` > `props` > `state`(通过api更新的状态以及useVbenModal参数)。如果你已经传入了 `slot` 或者 `props`,那么 `setState` 将不会生效,这种情况下你可以通过 `slot` 或者 `props` 来更新状态。
- 如果你使用到了 `connectedComponent` 参数,那么会存在 2 个`useVbenModal`, 此时,如果同时设置了相同的参数,那么以内部为准(也就是没有设置 connectedComponent 的代码)。比如 同时设置了 `onConfirm`,那么以内部的 `onConfirm` 为准。`onOpenChange`事件除外,内外都会触发。 - 如果你使用到了 `connectedComponent` 参数,那么会存在 2 个`useVbenModal`, 此时,如果同时设置了相同的参数,那么以内部为准(也就是没有设置 connectedComponent 的代码)。比如 同时设置了 `onConfirm`,那么以内部的 `onConfirm` 为准。`onOpenChange`事件除外,内外都会触发。
- 使用了`connectedComponent`参数时,可以配置`destroyOnClose`属性来决定当关闭弹窗时,是否要销毁`connectedComponent`组件(重新创建`connectedComponent`组件,这将会把其内部所有的变量、状态、数据等恢复到初始状态。)。
::: :::
@ -81,6 +82,8 @@ const [Modal, modalApi] = useVbenModal({
| 属性名 | 描述 | 类型 | 默认值 | | 属性名 | 描述 | 类型 | 默认值 |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| appendToMain | 是否挂载到内容区域默认挂载到body | `boolean` | `false` | | appendToMain | 是否挂载到内容区域默认挂载到body | `boolean` | `false` |
| connectedComponent | 连接另一个Modal组件 | `Component` | - |
| destroyOnClose | 关闭时销毁`connectedComponent` | `boolean` | `false` |
| title | 标题 | `string\|slot` | - | | title | 标题 | `string\|slot` | - |
| titleTooltip | 标题提示信息 | `string\|slot` | - | | titleTooltip | 标题提示信息 | `string\|slot` | - |
| description | 描述信息 | `string\|slot` | - | | description | 描述信息 | `string\|slot` | - |

View File

@ -2,6 +2,7 @@ type LayoutType =
| 'full-content' | 'full-content'
| 'header-mixed-nav' | 'header-mixed-nav'
| 'header-nav' | 'header-nav'
| 'header-sidebar-nav'
| 'mixed-nav' | 'mixed-nav'
| 'sidebar-mixed-nav' | 'sidebar-mixed-nav'
| 'sidebar-nav'; | 'sidebar-nav';

View File

@ -82,10 +82,20 @@ function usePreferences() {
() => appPreferences.value.layout === 'header-nav', () => appPreferences.value.layout === 'header-nav',
); );
/**
* @zh_CN
*/
const isHeaderMixedNav = computed( const isHeaderMixedNav = computed(
() => appPreferences.value.layout === 'header-mixed-nav', () => appPreferences.value.layout === 'header-mixed-nav',
); );
/**
* @zh_CN +
*/
const isHeaderSidebarNav = computed(
() => appPreferences.value.layout === 'header-sidebar-nav',
);
/** /**
* @zh_CN * @zh_CN
*/ */
@ -225,6 +235,7 @@ function usePreferences() {
isFullContent, isFullContent,
isHeaderMixedNav, isHeaderMixedNav,
isHeaderNav, isHeaderNav,
isHeaderSidebarNav,
isMixedNav, isMixedNav,
isMobile, isMobile,
isSideMixedNav, isSideMixedNav,

View File

@ -29,7 +29,11 @@ export function useLayout(props: VbenLayoutProps) {
/** /**
* *
*/ */
const isMixedNav = computed(() => currentLayout.value === 'mixed-nav'); const isMixedNav = computed(
() =>
currentLayout.value === 'mixed-nav' ||
currentLayout.value === 'header-sidebar-nav',
);
/** /**
* *

View File

@ -183,7 +183,8 @@ const isSideMode = computed(
currentLayout.value === 'mixed-nav' || currentLayout.value === 'mixed-nav' ||
currentLayout.value === 'sidebar-mixed-nav' || currentLayout.value === 'sidebar-mixed-nav' ||
currentLayout.value === 'sidebar-nav' || currentLayout.value === 'sidebar-nav' ||
currentLayout.value === 'header-mixed-nav', currentLayout.value === 'header-mixed-nav' ||
currentLayout.value === 'header-sidebar-nav',
); );
/** /**
@ -215,6 +216,7 @@ const mainStyle = computed(() => {
headerFixed.value && headerFixed.value &&
currentLayout.value !== 'header-nav' && currentLayout.value !== 'header-nav' &&
currentLayout.value !== 'mixed-nav' && currentLayout.value !== 'mixed-nav' &&
currentLayout.value !== 'header-sidebar-nav' &&
showSidebar.value && showSidebar.value &&
!props.isMobile !props.isMobile
) { ) {

View File

@ -110,4 +110,19 @@ describe('drawerApi', () => {
expect(drawerApi.store.state.title).toBe('Batch Title'); expect(drawerApi.store.state.title).toBe('Batch Title');
expect(drawerApi.store.state.confirmText).toBe('Batch Confirm'); expect(drawerApi.store.state.confirmText).toBe('Batch Confirm');
}); });
it('should call onClosed callback when provided', () => {
const onClosed = vi.fn();
const drawerApiWithHook = new DrawerApi({ onClosed });
drawerApiWithHook.onClosed();
expect(onClosed).toHaveBeenCalled();
});
it('should call onOpened callback when provided', () => {
const onOpened = vi.fn();
const drawerApiWithHook = new DrawerApi({ onOpened });
drawerApiWithHook.open();
drawerApiWithHook.onOpened();
expect(onOpened).toHaveBeenCalled();
});
}); });

View File

@ -6,7 +6,12 @@ import { bindMethods, isFunction } from '@vben-core/shared/utils';
export class DrawerApi { export class DrawerApi {
private api: Pick< private api: Pick<
DrawerApiOptions, DrawerApiOptions,
'onBeforeClose' | 'onCancel' | 'onConfirm' | 'onOpenChange' | 'onBeforeClose'
| 'onCancel'
| 'onClosed'
| 'onConfirm'
| 'onOpenChange'
| 'onOpened'
>; >;
// private prevState!: DrawerState; // private prevState!: DrawerState;
private state!: DrawerState; private state!: DrawerState;
@ -23,8 +28,10 @@ export class DrawerApi {
connectedComponent: _, connectedComponent: _,
onBeforeClose, onBeforeClose,
onCancel, onCancel,
onClosed,
onConfirm, onConfirm,
onOpenChange, onOpenChange,
onOpened,
...storeState ...storeState
} = options; } = options;
@ -68,8 +75,10 @@ export class DrawerApi {
this.api = { this.api = {
onBeforeClose, onBeforeClose,
onCancel, onCancel,
onClosed,
onConfirm, onConfirm,
onOpenChange, onOpenChange,
onOpened,
}; };
bindMethods(this); bindMethods(this);
} }
@ -114,6 +123,15 @@ export class DrawerApi {
} }
} }
/**
*
*/
onClosed() {
if (!this.state.isOpen) {
this.api.onClosed?.();
}
}
/** /**
* *
*/ */
@ -121,6 +139,15 @@ export class DrawerApi {
this.api.onConfirm?.(); this.api.onConfirm?.();
} }
/**
*
*/
onOpened() {
if (this.state.isOpen) {
this.api.onOpened?.();
}
}
open() { open() {
this.store.setState((prev) => ({ ...prev, isOpen: true })); this.store.setState((prev) => ({ ...prev, isOpen: true }));
} }

View File

@ -6,6 +6,8 @@ import type { Component, Ref } from 'vue';
export type DrawerPlacement = 'bottom' | 'left' | 'right' | 'top'; export type DrawerPlacement = 'bottom' | 'left' | 'right' | 'top';
export type CloseIconPlacement = 'left' | 'right';
export interface DrawerProps { export interface DrawerProps {
/** /**
* *
@ -18,10 +20,14 @@ export interface DrawerProps {
cancelText?: string; cancelText?: string;
class?: ClassType; class?: ClassType;
/** /**
* *
* @default true * @default true
*/ */
closable?: boolean; closable?: boolean;
/**
*
*/
closeIconPlacement?: CloseIconPlacement;
/** /**
* *
* @default true * @default true
@ -126,9 +132,13 @@ export type ExtendedDrawerApi = {
export interface DrawerApiOptions extends DrawerState { export interface DrawerApiOptions extends DrawerState {
/** /**
* *
*/ */
connectedComponent?: Component; connectedComponent?: Component;
/**
* 使 connectedComponent
*/
destroyOnClose?: boolean;
/** /**
* false * false
* @returns * @returns
@ -138,6 +148,11 @@ export interface DrawerApiOptions extends DrawerState {
* *
*/ */
onCancel?: () => void; onCancel?: () => void;
/**
*
* @returns
*/
onClosed?: () => void;
/** /**
* *
*/ */
@ -148,4 +163,9 @@ export interface DrawerApiOptions extends DrawerState {
* @returns * @returns
*/ */
onOpenChange?: (isOpen: boolean) => void; onOpenChange?: (isOpen: boolean) => void;
/**
*
* @returns
*/
onOpened?: () => void;
} }

View File

@ -10,6 +10,7 @@ import {
} from '@vben-core/composables'; } from '@vben-core/composables';
import { X } from '@vben-core/icons'; import { X } from '@vben-core/icons';
import { import {
Separator,
Sheet, Sheet,
SheetClose, SheetClose,
SheetContent, SheetContent,
@ -33,6 +34,7 @@ interface Props extends DrawerProps {
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
appendToMain: false, appendToMain: false,
closeIconPlacement: 'right',
drawerApi: undefined, drawerApi: undefined,
zIndex: 1000, zIndex: 1000,
}); });
@ -139,10 +141,12 @@ const getAppendTo = computed(() => {
:side="placement" :side="placement"
:z-index="zIndex" :z-index="zIndex"
@close-auto-focus="handleFocusOutside" @close-auto-focus="handleFocusOutside"
@closed="() => drawerApi?.onClosed()"
@escape-key-down="escapeKeyDown" @escape-key-down="escapeKeyDown"
@focus-outside="handleFocusOutside" @focus-outside="handleFocusOutside"
@interact-outside="interactOutside" @interact-outside="interactOutside"
@open-auto-focus="handerOpenAutoFocus" @open-auto-focus="handerOpenAutoFocus"
@opened="() => drawerApi?.onOpened()"
@pointer-down-outside="pointerDownOutside" @pointer-down-outside="pointerDownOutside"
> >
<SheetHeader <SheetHeader
@ -153,11 +157,29 @@ const getAppendTo = computed(() => {
headerClass, headerClass,
{ {
'px-4 py-3': closable, 'px-4 py-3': closable,
'pl-2': closable && closeIconPlacement === 'left',
}, },
) )
" "
> >
<div> <div class="flex items-center">
<SheetClose
v-if="closable && closeIconPlacement === 'left'"
as-child
class="data-[state=open]:bg-secondary ml-[2px] cursor-pointer rounded-full opacity-80 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none"
>
<slot name="close-icon">
<VbenIconButton>
<X class="size-4" />
</VbenIconButton>
</slot>
</SheetClose>
<Separator
v-if="closable && closeIconPlacement === 'left'"
class="ml-1 mr-2 h-8"
decorative
orientation="vertical"
/>
<SheetTitle v-if="title" class="text-left"> <SheetTitle v-if="title" class="text-left">
<slot name="title"> <slot name="title">
{{ title }} {{ title }}
@ -182,13 +204,15 @@ const getAppendTo = computed(() => {
<div class="flex-center"> <div class="flex-center">
<slot name="extra"></slot> <slot name="extra"></slot>
<SheetClose <SheetClose
v-if="closable" v-if="closable && closeIconPlacement === 'right'"
as-child as-child
class="data-[state=open]:bg-secondary ml-[2px] cursor-pointer rounded-full opacity-80 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none" class="data-[state=open]:bg-secondary ml-[2px] cursor-pointer rounded-full opacity-80 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none"
> >
<slot name="close-icon">
<VbenIconButton> <VbenIconButton>
<X class="size-4" /> <X class="size-4" />
</VbenIconButton> </VbenIconButton>
</slot>
</SheetClose> </SheetClose>
</div> </div>
</SheetHeader> </SheetHeader>

View File

@ -4,7 +4,15 @@ import type {
ExtendedDrawerApi, ExtendedDrawerApi,
} from './drawer'; } from './drawer';
import { defineComponent, h, inject, nextTick, provide, reactive } from 'vue'; import {
defineComponent,
h,
inject,
nextTick,
provide,
reactive,
ref,
} from 'vue';
import { useStore } from '@vben-core/shared/store'; import { useStore } from '@vben-core/shared/store';
@ -22,6 +30,7 @@ export function useVbenDrawer<
const { connectedComponent } = options; const { connectedComponent } = options;
if (connectedComponent) { if (connectedComponent) {
const extendedApi = reactive({}); const extendedApi = reactive({});
const isDrawerReady = ref(true);
const Drawer = defineComponent( const Drawer = defineComponent(
(props: TParentDrawerProps, { attrs, slots }) => { (props: TParentDrawerProps, { attrs, slots }) => {
provide(USER_DRAWER_INJECT_KEY, { provide(USER_DRAWER_INJECT_KEY, {
@ -31,13 +40,23 @@ export function useVbenDrawer<
Object.setPrototypeOf(extendedApi, api); Object.setPrototypeOf(extendedApi, api);
}, },
options, options,
async reCreateDrawer() {
isDrawerReady.value = false;
await nextTick();
isDrawerReady.value = true;
},
}); });
checkProps(extendedApi as ExtendedDrawerApi, { checkProps(extendedApi as ExtendedDrawerApi, {
...props, ...props,
...attrs, ...attrs,
...slots, ...slots,
}); });
return () => h(connectedComponent, { ...props, ...attrs }, slots); return () =>
h(
isDrawerReady.value ? connectedComponent : 'div',
{ ...props, ...attrs },
slots,
);
}, },
{ {
inheritAttrs: false, inheritAttrs: false,
@ -58,6 +77,14 @@ export function useVbenDrawer<
options.onOpenChange?.(isOpen); options.onOpenChange?.(isOpen);
injectData.options?.onOpenChange?.(isOpen); injectData.options?.onOpenChange?.(isOpen);
}; };
const onClosed = mergedOptions.onClosed;
mergedOptions.onClosed = () => {
onClosed?.();
if (mergedOptions.destroyOnClose) {
injectData.reCreateDrawer?.();
}
};
const api = new DrawerApi(mergedOptions); const api = new DrawerApi(mergedOptions);
const extendedApi: ExtendedDrawerApi = api as never; const extendedApi: ExtendedDrawerApi = api as never;

View File

@ -143,6 +143,10 @@ export interface ModalApiOptions extends ModalState {
* *
*/ */
connectedComponent?: Component; connectedComponent?: Component;
/**
* 使 connectedComponent
*/
destroyOnClose?: boolean;
/** /**
* false * false
* @returns * @returns

View File

@ -1,6 +1,14 @@
import type { ExtendedModalApi, ModalApiOptions, ModalProps } from './modal'; import type { ExtendedModalApi, ModalApiOptions, ModalProps } from './modal';
import { defineComponent, h, inject, nextTick, provide, reactive } from 'vue'; import {
defineComponent,
h,
inject,
nextTick,
provide,
reactive,
ref,
} from 'vue';
import { useStore } from '@vben-core/shared/store'; import { useStore } from '@vben-core/shared/store';
@ -18,6 +26,7 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
const { connectedComponent } = options; const { connectedComponent } = options;
if (connectedComponent) { if (connectedComponent) {
const extendedApi = reactive({}); const extendedApi = reactive({});
const isModalReady = ref(true);
const Modal = defineComponent( const Modal = defineComponent(
(props: TParentModalProps, { attrs, slots }) => { (props: TParentModalProps, { attrs, slots }) => {
provide(USER_MODAL_INJECT_KEY, { provide(USER_MODAL_INJECT_KEY, {
@ -27,6 +36,11 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
Object.setPrototypeOf(extendedApi, api); Object.setPrototypeOf(extendedApi, api);
}, },
options, options,
async reCreateModal() {
isModalReady.value = false;
await nextTick();
isModalReady.value = true;
},
}); });
checkProps(extendedApi as ExtendedModalApi, { checkProps(extendedApi as ExtendedModalApi, {
...props, ...props,
@ -35,7 +49,7 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
}); });
return () => return () =>
h( h(
connectedComponent, isModalReady.value ? connectedComponent : 'div',
{ {
...props, ...props,
...attrs, ...attrs,
@ -62,6 +76,15 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
options.onOpenChange?.(isOpen); options.onOpenChange?.(isOpen);
injectData.options?.onOpenChange?.(isOpen); injectData.options?.onOpenChange?.(isOpen);
}; };
const onClosed = mergedOptions.onClosed;
mergedOptions.onClosed = () => {
onClosed?.();
if (mergedOptions.destroyOnClose) {
injectData.reCreateModal?.();
}
};
const api = new ModalApi(mergedOptions); const api = new ModalApi(mergedOptions);
const extendedApi: ExtendedModalApi = api as never; const extendedApi: ExtendedModalApi = api as never;

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { computed, ref } from 'vue';
import { cn } from '@vben-core/shared/utils'; import { cn } from '@vben-core/shared/utils';
@ -32,7 +32,9 @@ const props = withDefaults(defineProps<SheetContentProps>(), {
zIndex: 1000, zIndex: 1000,
}); });
const emits = defineEmits<DialogContentEmits>(); const emits = defineEmits<
{ close: []; closed: []; opened: [] } & DialogContentEmits
>();
const delegatedProps = computed(() => { const delegatedProps = computed(() => {
const { const {
@ -59,6 +61,17 @@ const position = computed(() => {
}); });
const forwarded = useForwardPropsEmits(delegatedProps, emits); const forwarded = useForwardPropsEmits(delegatedProps, emits);
const contentRef = ref<InstanceType<typeof DialogContent> | null>(null);
function onAnimationEnd(event: AnimationEvent) {
// contentRef opened/closed
if (event.target === contentRef.value?.$el) {
if (props.open) {
emits('opened');
} else {
emits('closed');
}
}
}
</script> </script>
<template> <template>
@ -67,8 +80,10 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
<SheetOverlay v-if="open && modal" :style="{ zIndex, position }" /> <SheetOverlay v-if="open && modal" :style="{ zIndex, position }" />
</Transition> </Transition>
<DialogContent <DialogContent
ref="contentRef"
:class="cn(sheetVariants({ side }), props.class)" :class="cn(sheetVariants({ side }), props.class)"
:style="{ zIndex, position }" :style="{ zIndex, position }"
@animationend="onAnimationEnd"
v-bind="{ ...forwarded, ...$attrs }" v-bind="{ ...forwarded, ...$attrs }"
> >
<slot></slot> <slot></slot>

View File

@ -40,6 +40,7 @@ const {
isMobile, isMobile,
isSideMixedNav, isSideMixedNav,
isHeaderMixedNav, isHeaderMixedNav,
isHeaderSidebarNav,
layout, layout,
preferencesButtonPosition, preferencesButtonPosition,
sidebarCollapsed, sidebarCollapsed,
@ -81,7 +82,7 @@ const logoCollapsed = computed(() => {
if (isMobile.value && sidebarCollapsed.value) { if (isMobile.value && sidebarCollapsed.value) {
return true; return true;
} }
if (isHeaderNav.value || isMixedNav.value) { if (isHeaderNav.value || isMixedNav.value || isHeaderSidebarNav.value) {
return false; return false;
} }
return ( return (

View File

@ -11,6 +11,7 @@ import {
FullContent, FullContent,
HeaderMixedNav, HeaderMixedNav,
HeaderNav, HeaderNav,
HeaderSidebarNav,
MixedNav, MixedNav,
SidebarMixedNav, SidebarMixedNav,
SidebarNav, SidebarNav,
@ -35,6 +36,7 @@ const components: Record<LayoutType, Component> = {
'sidebar-mixed-nav': SidebarMixedNav, 'sidebar-mixed-nav': SidebarMixedNav,
'sidebar-nav': SidebarNav, 'sidebar-nav': SidebarNav,
'header-mixed-nav': HeaderMixedNav, 'header-mixed-nav': HeaderMixedNav,
'header-sidebar-nav': HeaderSidebarNav,
}; };
const PRESET = computed((): PresetItem[] => [ const PRESET = computed((): PresetItem[] => [
@ -53,6 +55,11 @@ const PRESET = computed((): PresetItem[] => [
tip: $t('preferences.horizontalTip'), tip: $t('preferences.horizontalTip'),
type: 'header-nav', type: 'header-nav',
}, },
{
name: $t('preferences.headerSidebarNav'),
tip: $t('preferences.headerSidebarNavTip'),
type: 'header-sidebar-nav',
},
{ {
name: $t('preferences.mixedMenu'), name: $t('preferences.mixedMenu'),
tip: $t('preferences.mixedMenuTip'), tip: $t('preferences.mixedMenuTip'),

View File

@ -0,0 +1,177 @@
<template>
<svg
class="custom-radio-image"
fill="none"
height="66"
width="104"
xmlns="http://www.w3.org/2000/svg"
>
<g>
<rect
id="svg_1"
fill="currentColor"
fill-opacity="0.02"
height="66"
rx="4"
stroke="null"
width="104"
x="0.13514"
y="0.13514"
/>
<rect
id="svg_8"
fill="currentColor"
fill-opacity="0.08"
height="9.07027"
stroke="null"
width="104.07934"
x="-0.07419"
y="-0.05773"
/>
<rect
id="svg_3"
fill="#b2b2b2"
height="1.689"
rx="1.395"
stroke="null"
width="6.52486"
x="10.08168"
y="3.50832"
/>
<rect
id="svg_10"
fill="#b2b2b2"
height="4.4"
rx="1"
stroke="null"
width="3.925"
x="80.75054"
y="2.89362"
/>
<rect
id="svg_11"
fill="#b2b2b2"
height="4.4"
rx="1"
stroke="null"
width="3.925"
x="87.58249"
y="2.89362"
/>
<path
id="svg_12"
d="m98.19822,2.872c0,-0.54338 0.45662,-1 1,-1l1.925,0c0.54338,0 1,0.45662 1,1l0,2.4c0,0.54338 -0.45662,1 -1,1l-1.925,0c-0.54338,0 -1,-0.45662 -1,-1l0,-2.4z"
fill="#ffffff"
opacity="undefined"
stroke="null"
/>
<rect
id="svg_13"
fill="currentColor"
fill-opacity="0.08"
height="21.51892"
rx="2"
stroke="null"
width="44.13071"
x="53.37873"
y="13.45652"
/>
<path
id="svg_14"
d="m19.4393,15.74245c0,-1.08676 0.79001,-2 1.73013,-2l23.18605,0c0.94011,0 1.73013,0.91324 1.73013,2l0,17.24865c0,1.08676 -0.79001,2 -1.73013,2l-23.18605,0c-0.94011,0 -1.73013,-0.91324 -1.73013,-2l0,-17.24865z"
fill="currentColor"
fill-opacity="0.08"
opacity="undefined"
stroke="null"
/>
<rect
id="svg_15"
fill="currentColor"
fill-opacity="0.08"
height="21.65405"
rx="2"
stroke="null"
width="78.39372"
x="19.93575"
y="39.34689"
/>
<rect
id="svg_21"
fill="#e5e5e5"
height="2.789"
rx="1.395"
stroke="null"
width="7.52486"
x="28.14924"
y="3.07319"
/>
<rect
id="svg_22"
fill="#e5e5e5"
height="2.789"
rx="1.395"
stroke="null"
width="7.52486"
x="41.25735"
y="3.20832"
/>
<rect
id="svg_23"
fill="#e5e5e5"
height="2.789"
rx="1.395"
stroke="null"
width="7.52486"
x="54.23033"
y="3.07319"
/>
<rect
id="svg_4"
fill="#ffffff"
height="5.13843"
rx="2"
stroke="null"
width="5.78397"
x="1.5327"
y="1.081"
/>
<rect
id="svg_5"
fill="hsl(var(--primary))"
height="56.81191"
stroke="null"
width="15.44642"
x="-0.06423"
y="9.03113"
/>
<path
id="svg_2"
d="m2.38669,15.38074c0,-0.20384 0.27195,-0.37513 0.59557,-0.37513l7.98149,0c0.32362,0 0.59557,0.17129 0.59557,0.37513l0,3.23525c0,0.20384 -0.27195,0.37513 -0.59557,0.37513l-7.98149,0c-0.32362,0 -0.59557,-0.17129 -0.59557,-0.37513l0,-3.23525z"
fill="#fff"
opacity="undefined"
stroke="null"
/>
<path
id="svg_6"
d="m2.38669,28.43336c0,-0.20384 0.27195,-0.37513 0.59557,-0.37513l7.98149,0c0.32362,0 0.59557,0.17129 0.59557,0.37513l0,3.23525c0,0.20384 -0.27195,0.37513 -0.59557,0.37513l-7.98149,0c-0.32362,0 -0.59557,-0.17129 -0.59557,-0.37513l0,-3.23525z"
fill="#fff"
opacity="undefined"
stroke="null"
/>
<path
id="svg_7"
d="m2.17616,41.27545c0,-0.20384 0.27195,-0.37513 0.59557,-0.37513l7.98149,0c0.32362,0 0.59557,0.17129 0.59557,0.37513l0,3.23525c0,0.20384 -0.27195,0.37513 -0.59557,0.37513l-7.98149,0c-0.32362,0 -0.59557,-0.17129 -0.59557,-0.37513l0,-3.23525z"
fill="#fff"
opacity="undefined"
stroke="null"
/>
<path
id="svg_9"
d="m2.17616,54.32806c0,-0.20384 0.27195,-0.37513 0.59557,-0.37513l7.98149,0c0.32362,0 0.59557,0.17129 0.59557,0.37513l0,3.23525c0,0.20384 -0.27195,0.37513 -0.59557,0.37513l-7.98149,0c-0.32362,0 -0.59557,-0.17129 -0.59557,-0.37513l0,-3.23525z"
fill="#fff"
opacity="undefined"
stroke="null"
/>
</g>
</svg>
</template>

View File

@ -3,6 +3,7 @@ import HeaderNav from './header-nav.vue';
export { default as ContentCompact } from './content-compact.vue'; export { default as ContentCompact } from './content-compact.vue';
export { default as FullContent } from './full-content.vue'; export { default as FullContent } from './full-content.vue';
export { default as HeaderMixedNav } from './header-mixed-nav.vue'; export { default as HeaderMixedNav } from './header-mixed-nav.vue';
export { default as HeaderSidebarNav } from './header-sidebar-nav.vue';
export { default as MixedNav } from './mixed-nav.vue'; export { default as MixedNav } from './mixed-nav.vue';
export { default as SidebarMixedNav } from './sidebar-mixed-nav.vue'; export { default as SidebarMixedNav } from './sidebar-mixed-nav.vue';
export { default as SidebarNav } from './sidebar-nav.vue'; export { default as SidebarNav } from './sidebar-nav.vue';

View File

@ -162,6 +162,7 @@ const {
isDark, isDark,
isFullContent, isFullContent,
isHeaderNav, isHeaderNav,
isHeaderSidebarNav,
isMixedNav, isMixedNav,
isSideMixedNav, isSideMixedNav,
isSideMode, isSideMode,
@ -344,7 +345,8 @@ async function handleReset() {
v-model:breadcrumb-show-icon="breadcrumbShowIcon" v-model:breadcrumb-show-icon="breadcrumbShowIcon"
v-model:breadcrumb-style-type="breadcrumbStyleType" v-model:breadcrumb-style-type="breadcrumbStyleType"
:disabled=" :disabled="
!showBreadcrumbConfig || !(isSideNav || isSideMixedNav) !showBreadcrumbConfig ||
!(isSideNav || isSideMixedNav || isHeaderSidebarNav)
" "
/> />
</Block> </Block>

View File

@ -17,6 +17,8 @@
"horizontalTip": "Horizontal menu mode, all menus displayed at the top", "horizontalTip": "Horizontal menu mode, all menus displayed at the top",
"twoColumn": "Two Column", "twoColumn": "Two Column",
"twoColumnTip": "Vertical Two Column Menu Mode", "twoColumnTip": "Vertical Two Column Menu Mode",
"headerSidebarNav": "Header Vertical",
"headerSidebarNavTip": "Header Full Width, Sidebar Navigation Mode",
"mixedMenu": "Mixed Menu", "mixedMenu": "Mixed Menu",
"mixedMenuTip": "Vertical & Horizontal Menu Co-exists", "mixedMenuTip": "Vertical & Horizontal Menu Co-exists",
"fullContent": "Full Content", "fullContent": "Full Content",

View File

@ -17,6 +17,8 @@
"horizontalTip": "水平菜单模式,菜单全部显示在顶部", "horizontalTip": "水平菜单模式,菜单全部显示在顶部",
"twoColumn": "双列菜单", "twoColumn": "双列菜单",
"twoColumnTip": "垂直双列菜单模式", "twoColumnTip": "垂直双列菜单模式",
"headerSidebarNav": "侧边导航",
"headerSidebarNavTip": "顶部通栏,侧边导航模式",
"headerTwoColumn": "混合双列", "headerTwoColumn": "混合双列",
"headerTwoColumnTip": "双列、水平菜单共存模式", "headerTwoColumnTip": "双列、水平菜单共存模式",
"mixedMenu": "混合垂直", "mixedMenu": "混合垂直",