diff --git a/docs/src/components/common-ui/vben-drawer.md b/docs/src/components/common-ui/vben-drawer.md index 7091570f..e2409daa 100644 --- a/docs/src/components/common-ui/vben-drawer.md +++ b/docs/src/components/common-ui/vben-drawer.md @@ -54,6 +54,7 @@ Drawer 内的内容一般业务中,会比较复杂,所以我们可以将 dra - `VbenDrawer` 组件对与参数的处理优先级是 `slot` > `props` > `state`(通过api更新的状态以及useVbenDrawer参数)。如果你已经传入了 `slot` 或者 `props`,那么 `setState` 将不会生效,这种情况下你可以通过 `slot` 或者 `props` 来更新状态。 - 如果你使用到了 `connectedComponent` 参数,那么会存在 2 个`useVbenDrawer`, 此时,如果同时设置了相同的参数,那么以内部为准(也就是没有设置 connectedComponent 的代码)。比如 同时设置了 `onConfirm`,那么以内部的 `onConfirm` 为准。`onOpenChange`事件除外,内外都会触发。 +- 使用了`connectedComponent`参数时,可以配置`destroyOnClose`属性来决定当关闭弹窗时,是否要销毁`connectedComponent`组件(重新创建`connectedComponent`组件,这将会把其内部所有的变量、状态、数据等恢复到初始状态。)。 ::: @@ -75,12 +76,15 @@ const [Drawer, drawerApi] = useVbenDrawer({ | 属性名 | 描述 | 类型 | 默认值 | | --- | --- | --- | --- | | appendToMain | 是否挂载到内容区域(默认挂载到body) | `boolean` | `false` | +| connectedComponent | 连接另一个Modal组件 | `Component` | - | +| destroyOnClose | 关闭时销毁`connectedComponent` | `boolean` | `false` | | title | 标题 | `string\|slot` | - | | titleTooltip | 标题提示信息 | `string\|slot` | - | | description | 描述信息 | `string\|slot` | - | | isOpen | 弹窗打开状态 | `boolean` | `false` | | loading | 弹窗加载状态 | `boolean` | `false` | | closable | 显示关闭按钮 | `boolean` | `true` | +| closeIconPlacement | 关闭按钮位置 | `'left'\|'right'` | `right` | | modal | 显示遮罩 | `boolean` | `true` | | header | 显示header | `boolean` | `true` | | footer | 显示footer | `boolean\|slot` | `true` | @@ -108,12 +112,14 @@ const [Drawer, drawerApi] = useVbenDrawer({ 以下事件,只有在 `useVbenDrawer({onCancel:()=>{}})` 中传入才会生效。 -| 事件名 | 描述 | 类型 | -| --- | --- | --- | -| onBeforeClose | 关闭前触发,返回 `false`则禁止关闭 | `()=>boolean` | -| onCancel | 点击取消按钮触发 | `()=>void` | -| onConfirm | 点击确认按钮触发 | `()=>void` | -| onOpenChange | 关闭或者打开弹窗时触发 | `(isOpen:boolean)=>void` | +| 事件名 | 描述 | 类型 | 版本限制 | +| --- | --- | --- | --- | +| onBeforeClose | 关闭前触发,返回 `false`则禁止关闭 | `()=>boolean` | --- | +| onCancel | 点击取消按钮触发 | `()=>void` | --- | +| onClosed | 关闭动画播放完毕时触发 | `()=>void` | >5.5.2 | +| onConfirm | 点击确认按钮触发 | `()=>void` | --- | +| onOpenChange | 关闭或者打开弹窗时触发 | `(isOpen:boolean)=>void` | --- | +| onOpened | 打开动画播放完毕时触发 | `()=>void` | >5.5.2 | ### Slots @@ -124,6 +130,8 @@ const [Drawer, drawerApi] = useVbenDrawer({ | default | 默认插槽 - 弹窗内容 | | prepend-footer | 取消按钮左侧 | | append-footer | 取消按钮右侧 | +| close-icon | 关闭按钮图标 | +| extra | 额外内容(标题右侧) | ### modalApi diff --git a/docs/src/components/common-ui/vben-modal.md b/docs/src/components/common-ui/vben-modal.md index d6e3ef47..9e422d18 100644 --- a/docs/src/components/common-ui/vben-modal.md +++ b/docs/src/components/common-ui/vben-modal.md @@ -60,6 +60,7 @@ Modal 内的内容一般业务中,会比较复杂,所以我们可以将 moda - `VbenModal` 组件对与参数的处理优先级是 `slot` > `props` > `state`(通过api更新的状态以及useVbenModal参数)。如果你已经传入了 `slot` 或者 `props`,那么 `setState` 将不会生效,这种情况下你可以通过 `slot` 或者 `props` 来更新状态。 - 如果你使用到了 `connectedComponent` 参数,那么会存在 2 个`useVbenModal`, 此时,如果同时设置了相同的参数,那么以内部为准(也就是没有设置 connectedComponent 的代码)。比如 同时设置了 `onConfirm`,那么以内部的 `onConfirm` 为准。`onOpenChange`事件除外,内外都会触发。 +- 使用了`connectedComponent`参数时,可以配置`destroyOnClose`属性来决定当关闭弹窗时,是否要销毁`connectedComponent`组件(重新创建`connectedComponent`组件,这将会把其内部所有的变量、状态、数据等恢复到初始状态。)。 ::: @@ -81,6 +82,8 @@ const [Modal, modalApi] = useVbenModal({ | 属性名 | 描述 | 类型 | 默认值 | | --- | --- | --- | --- | | appendToMain | 是否挂载到内容区域(默认挂载到body) | `boolean` | `false` | +| connectedComponent | 连接另一个Modal组件 | `Component` | - | +| destroyOnClose | 关闭时销毁`connectedComponent` | `boolean` | `false` | | title | 标题 | `string\|slot` | - | | titleTooltip | 标题提示信息 | `string\|slot` | - | | description | 描述信息 | `string\|slot` | - | diff --git a/packages/@core/base/typings/src/app.d.ts b/packages/@core/base/typings/src/app.d.ts index ae49c786..02da2466 100644 --- a/packages/@core/base/typings/src/app.d.ts +++ b/packages/@core/base/typings/src/app.d.ts @@ -2,6 +2,7 @@ type LayoutType = | 'full-content' | 'header-mixed-nav' | 'header-nav' + | 'header-sidebar-nav' | 'mixed-nav' | 'sidebar-mixed-nav' | 'sidebar-nav'; diff --git a/packages/@core/preferences/src/use-preferences.ts b/packages/@core/preferences/src/use-preferences.ts index ecd2bc74..20df7a60 100644 --- a/packages/@core/preferences/src/use-preferences.ts +++ b/packages/@core/preferences/src/use-preferences.ts @@ -82,10 +82,20 @@ function usePreferences() { () => appPreferences.value.layout === 'header-nav', ); + /** + * @zh_CN 是否为头部混合导航模式 + */ const isHeaderMixedNav = computed( () => appPreferences.value.layout === 'header-mixed-nav', ); + /** + * @zh_CN 是否为顶部通栏+侧边导航模式 + */ + const isHeaderSidebarNav = computed( + () => appPreferences.value.layout === 'header-sidebar-nav', + ); + /** * @zh_CN 是否为混合导航模式 */ @@ -225,6 +235,7 @@ function usePreferences() { isFullContent, isHeaderMixedNav, isHeaderNav, + isHeaderSidebarNav, isMixedNav, isMobile, isSideMixedNav, diff --git a/packages/@core/ui-kit/layout-ui/src/hooks/use-layout.ts b/packages/@core/ui-kit/layout-ui/src/hooks/use-layout.ts index 7b758dc4..2f2b82bb 100644 --- a/packages/@core/ui-kit/layout-ui/src/hooks/use-layout.ts +++ b/packages/@core/ui-kit/layout-ui/src/hooks/use-layout.ts @@ -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', + ); /** * 是否为头部混合模式 diff --git a/packages/@core/ui-kit/layout-ui/src/vben-layout.vue b/packages/@core/ui-kit/layout-ui/src/vben-layout.vue index dee990b3..8821bfab 100644 --- a/packages/@core/ui-kit/layout-ui/src/vben-layout.vue +++ b/packages/@core/ui-kit/layout-ui/src/vben-layout.vue @@ -183,7 +183,8 @@ const isSideMode = computed( currentLayout.value === 'mixed-nav' || currentLayout.value === 'sidebar-mixed-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 && currentLayout.value !== 'header-nav' && currentLayout.value !== 'mixed-nav' && + currentLayout.value !== 'header-sidebar-nav' && showSidebar.value && !props.isMobile ) { diff --git a/packages/@core/ui-kit/popup-ui/src/drawer/__tests__/drawer-api.test.ts b/packages/@core/ui-kit/popup-ui/src/drawer/__tests__/drawer-api.test.ts index 8d715ff8..b55a9dac 100644 --- a/packages/@core/ui-kit/popup-ui/src/drawer/__tests__/drawer-api.test.ts +++ b/packages/@core/ui-kit/popup-ui/src/drawer/__tests__/drawer-api.test.ts @@ -110,4 +110,19 @@ describe('drawerApi', () => { expect(drawerApi.store.state.title).toBe('Batch Title'); 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(); + }); }); diff --git a/packages/@core/ui-kit/popup-ui/src/drawer/drawer-api.ts b/packages/@core/ui-kit/popup-ui/src/drawer/drawer-api.ts index ca23d105..68aaacba 100644 --- a/packages/@core/ui-kit/popup-ui/src/drawer/drawer-api.ts +++ b/packages/@core/ui-kit/popup-ui/src/drawer/drawer-api.ts @@ -6,7 +6,12 @@ import { bindMethods, isFunction } from '@vben-core/shared/utils'; export class DrawerApi { private api: Pick< DrawerApiOptions, - 'onBeforeClose' | 'onCancel' | 'onConfirm' | 'onOpenChange' + | 'onBeforeClose' + | 'onCancel' + | 'onClosed' + | 'onConfirm' + | 'onOpenChange' + | 'onOpened' >; // private prevState!: DrawerState; private state!: DrawerState; @@ -23,8 +28,10 @@ export class DrawerApi { connectedComponent: _, onBeforeClose, onCancel, + onClosed, onConfirm, onOpenChange, + onOpened, ...storeState } = options; @@ -68,8 +75,10 @@ export class DrawerApi { this.api = { onBeforeClose, onCancel, + onClosed, onConfirm, onOpenChange, + onOpened, }; 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?.(); } + /** + * 弹窗打开动画播放完毕后的回调 + */ + onOpened() { + if (this.state.isOpen) { + this.api.onOpened?.(); + } + } + open() { this.store.setState((prev) => ({ ...prev, isOpen: true })); } diff --git a/packages/@core/ui-kit/popup-ui/src/drawer/drawer.ts b/packages/@core/ui-kit/popup-ui/src/drawer/drawer.ts index f75ee50c..9e1578de 100644 --- a/packages/@core/ui-kit/popup-ui/src/drawer/drawer.ts +++ b/packages/@core/ui-kit/popup-ui/src/drawer/drawer.ts @@ -6,6 +6,8 @@ import type { Component, Ref } from 'vue'; export type DrawerPlacement = 'bottom' | 'left' | 'right' | 'top'; +export type CloseIconPlacement = 'left' | 'right'; + export interface DrawerProps { /** * 是否挂载到内容区域 @@ -18,10 +20,14 @@ export interface DrawerProps { cancelText?: string; class?: ClassType; /** - * 是否显示右上角的关闭按钮 + * 是否显示关闭按钮 * @default true */ closable?: boolean; + /** + * 关闭按钮的位置 + */ + closeIconPlacement?: CloseIconPlacement; /** * 点击弹窗遮罩是否关闭弹窗 * @default true @@ -126,9 +132,13 @@ export type ExtendedDrawerApi = { export interface DrawerApiOptions extends DrawerState { /** - * 独立的弹窗组件 + * 独立的抽屉组件 */ connectedComponent?: Component; + /** + * 在关闭时销毁抽屉。仅在使用 connectedComponent 时有效 + */ + destroyOnClose?: boolean; /** * 关闭前的回调,返回 false 可以阻止关闭 * @returns @@ -138,6 +148,11 @@ export interface DrawerApiOptions extends DrawerState { * 点击取消按钮的回调 */ onCancel?: () => void; + /** + * 弹窗关闭动画结束的回调 + * @returns + */ + onClosed?: () => void; /** * 点击确定按钮的回调 */ @@ -148,4 +163,9 @@ export interface DrawerApiOptions extends DrawerState { * @returns */ onOpenChange?: (isOpen: boolean) => void; + /** + * 弹窗打开动画结束的回调 + * @returns + */ + onOpened?: () => void; } diff --git a/packages/@core/ui-kit/popup-ui/src/drawer/drawer.vue b/packages/@core/ui-kit/popup-ui/src/drawer/drawer.vue index 31a40988..30687bd3 100644 --- a/packages/@core/ui-kit/popup-ui/src/drawer/drawer.vue +++ b/packages/@core/ui-kit/popup-ui/src/drawer/drawer.vue @@ -10,6 +10,7 @@ import { } from '@vben-core/composables'; import { X } from '@vben-core/icons'; import { + Separator, Sheet, SheetClose, SheetContent, @@ -33,6 +34,7 @@ interface Props extends DrawerProps { const props = withDefaults(defineProps(), { appendToMain: false, + closeIconPlacement: 'right', drawerApi: undefined, zIndex: 1000, }); @@ -139,10 +141,12 @@ const getAppendTo = computed(() => { :side="placement" :z-index="zIndex" @close-auto-focus="handleFocusOutside" + @closed="() => drawerApi?.onClosed()" @escape-key-down="escapeKeyDown" @focus-outside="handleFocusOutside" @interact-outside="interactOutside" @open-auto-focus="handerOpenAutoFocus" + @opened="() => drawerApi?.onOpened()" @pointer-down-outside="pointerDownOutside" > { headerClass, { 'px-4 py-3': closable, + 'pl-2': closable && closeIconPlacement === 'left', }, ) " > -
+
+ + + + + + + + {{ title }} @@ -182,13 +204,15 @@ const getAppendTo = computed(() => {
- - - + + + + +
diff --git a/packages/@core/ui-kit/popup-ui/src/drawer/use-drawer.ts b/packages/@core/ui-kit/popup-ui/src/drawer/use-drawer.ts index c3ff263b..ad8c58cd 100644 --- a/packages/@core/ui-kit/popup-ui/src/drawer/use-drawer.ts +++ b/packages/@core/ui-kit/popup-ui/src/drawer/use-drawer.ts @@ -4,7 +4,15 @@ import type { ExtendedDrawerApi, } 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'; @@ -22,6 +30,7 @@ export function useVbenDrawer< const { connectedComponent } = options; if (connectedComponent) { const extendedApi = reactive({}); + const isDrawerReady = ref(true); const Drawer = defineComponent( (props: TParentDrawerProps, { attrs, slots }) => { provide(USER_DRAWER_INJECT_KEY, { @@ -31,13 +40,23 @@ export function useVbenDrawer< Object.setPrototypeOf(extendedApi, api); }, options, + async reCreateDrawer() { + isDrawerReady.value = false; + await nextTick(); + isDrawerReady.value = true; + }, }); checkProps(extendedApi as ExtendedDrawerApi, { ...props, ...attrs, ...slots, }); - return () => h(connectedComponent, { ...props, ...attrs }, slots); + return () => + h( + isDrawerReady.value ? connectedComponent : 'div', + { ...props, ...attrs }, + slots, + ); }, { inheritAttrs: false, @@ -58,6 +77,14 @@ export function useVbenDrawer< 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 extendedApi: ExtendedDrawerApi = api as never; diff --git a/packages/@core/ui-kit/popup-ui/src/modal/modal.ts b/packages/@core/ui-kit/popup-ui/src/modal/modal.ts index 5508d004..ea761e43 100644 --- a/packages/@core/ui-kit/popup-ui/src/modal/modal.ts +++ b/packages/@core/ui-kit/popup-ui/src/modal/modal.ts @@ -143,6 +143,10 @@ export interface ModalApiOptions extends ModalState { * 独立的弹窗组件 */ connectedComponent?: Component; + /** + * 在关闭时销毁弹窗。仅在使用 connectedComponent 时有效 + */ + destroyOnClose?: boolean; /** * 关闭前的回调,返回 false 可以阻止关闭 * @returns diff --git a/packages/@core/ui-kit/popup-ui/src/modal/use-modal.ts b/packages/@core/ui-kit/popup-ui/src/modal/use-modal.ts index f972530d..aebdf945 100644 --- a/packages/@core/ui-kit/popup-ui/src/modal/use-modal.ts +++ b/packages/@core/ui-kit/popup-ui/src/modal/use-modal.ts @@ -1,6 +1,14 @@ 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'; @@ -18,6 +26,7 @@ export function useVbenModal( const { connectedComponent } = options; if (connectedComponent) { const extendedApi = reactive({}); + const isModalReady = ref(true); const Modal = defineComponent( (props: TParentModalProps, { attrs, slots }) => { provide(USER_MODAL_INJECT_KEY, { @@ -27,6 +36,11 @@ export function useVbenModal( Object.setPrototypeOf(extendedApi, api); }, options, + async reCreateModal() { + isModalReady.value = false; + await nextTick(); + isModalReady.value = true; + }, }); checkProps(extendedApi as ExtendedModalApi, { ...props, @@ -35,7 +49,7 @@ export function useVbenModal( }); return () => h( - connectedComponent, + isModalReady.value ? connectedComponent : 'div', { ...props, ...attrs, @@ -62,6 +76,15 @@ export function useVbenModal( 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 extendedApi: ExtendedModalApi = api as never; diff --git a/packages/@core/ui-kit/shadcn-ui/src/ui/sheet/SheetContent.vue b/packages/@core/ui-kit/shadcn-ui/src/ui/sheet/SheetContent.vue index 5b09675c..e15c60f9 100644 --- a/packages/@core/ui-kit/shadcn-ui/src/ui/sheet/SheetContent.vue +++ b/packages/@core/ui-kit/shadcn-ui/src/ui/sheet/SheetContent.vue @@ -1,5 +1,5 @@