Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev
This commit is contained in:
commit
cf9edbb1c4
@ -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
|
||||
|
||||
|
@ -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` | - |
|
||||
|
1
packages/@core/base/typings/src/app.d.ts
vendored
1
packages/@core/base/typings/src/app.d.ts
vendored
@ -2,6 +2,7 @@ type LayoutType =
|
||||
| 'full-content'
|
||||
| 'header-mixed-nav'
|
||||
| 'header-nav'
|
||||
| 'header-sidebar-nav'
|
||||
| 'mixed-nav'
|
||||
| 'sidebar-mixed-nav'
|
||||
| 'sidebar-nav';
|
||||
|
@ -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,
|
||||
|
@ -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',
|
||||
);
|
||||
|
||||
/**
|
||||
* 是否为头部混合模式
|
||||
|
@ -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
|
||||
) {
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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 }));
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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<Props>(), {
|
||||
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"
|
||||
>
|
||||
<SheetHeader
|
||||
@ -153,11 +157,29 @@ const getAppendTo = computed(() => {
|
||||
headerClass,
|
||||
{
|
||||
'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">
|
||||
<slot name="title">
|
||||
{{ title }}
|
||||
@ -182,13 +204,15 @@ const getAppendTo = computed(() => {
|
||||
<div class="flex-center">
|
||||
<slot name="extra"></slot>
|
||||
<SheetClose
|
||||
v-if="closable"
|
||||
v-if="closable && closeIconPlacement === 'right'"
|
||||
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"
|
||||
>
|
||||
<VbenIconButton>
|
||||
<X class="size-4" />
|
||||
</VbenIconButton>
|
||||
<slot name="close-icon">
|
||||
<VbenIconButton>
|
||||
<X class="size-4" />
|
||||
</VbenIconButton>
|
||||
</slot>
|
||||
</SheetClose>
|
||||
</div>
|
||||
</SheetHeader>
|
||||
|
@ -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;
|
||||
|
@ -143,6 +143,10 @@ export interface ModalApiOptions extends ModalState {
|
||||
* 独立的弹窗组件
|
||||
*/
|
||||
connectedComponent?: Component;
|
||||
/**
|
||||
* 在关闭时销毁弹窗。仅在使用 connectedComponent 时有效
|
||||
*/
|
||||
destroyOnClose?: boolean;
|
||||
/**
|
||||
* 关闭前的回调,返回 false 可以阻止关闭
|
||||
* @returns
|
||||
|
@ -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<TParentModalProps extends ModalProps = ModalProps>(
|
||||
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<TParentModalProps extends ModalProps = ModalProps>(
|
||||
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<TParentModalProps extends ModalProps = ModalProps>(
|
||||
});
|
||||
return () =>
|
||||
h(
|
||||
connectedComponent,
|
||||
isModalReady.value ? connectedComponent : 'div',
|
||||
{
|
||||
...props,
|
||||
...attrs,
|
||||
@ -62,6 +76,15 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
|
||||
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;
|
||||
|
@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
@ -32,7 +32,9 @@ const props = withDefaults(defineProps<SheetContentProps>(), {
|
||||
zIndex: 1000,
|
||||
});
|
||||
|
||||
const emits = defineEmits<DialogContentEmits>();
|
||||
const emits = defineEmits<
|
||||
{ close: []; closed: []; opened: [] } & DialogContentEmits
|
||||
>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const {
|
||||
@ -59,6 +61,17 @@ const position = computed(() => {
|
||||
});
|
||||
|
||||
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>
|
||||
|
||||
<template>
|
||||
@ -67,8 +80,10 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||
<SheetOverlay v-if="open && modal" :style="{ zIndex, position }" />
|
||||
</Transition>
|
||||
<DialogContent
|
||||
ref="contentRef"
|
||||
:class="cn(sheetVariants({ side }), props.class)"
|
||||
:style="{ zIndex, position }"
|
||||
@animationend="onAnimationEnd"
|
||||
v-bind="{ ...forwarded, ...$attrs }"
|
||||
>
|
||||
<slot></slot>
|
||||
|
@ -40,6 +40,7 @@ const {
|
||||
isMobile,
|
||||
isSideMixedNav,
|
||||
isHeaderMixedNav,
|
||||
isHeaderSidebarNav,
|
||||
layout,
|
||||
preferencesButtonPosition,
|
||||
sidebarCollapsed,
|
||||
@ -81,7 +82,7 @@ const logoCollapsed = computed(() => {
|
||||
if (isMobile.value && sidebarCollapsed.value) {
|
||||
return true;
|
||||
}
|
||||
if (isHeaderNav.value || isMixedNav.value) {
|
||||
if (isHeaderNav.value || isMixedNav.value || isHeaderSidebarNav.value) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
FullContent,
|
||||
HeaderMixedNav,
|
||||
HeaderNav,
|
||||
HeaderSidebarNav,
|
||||
MixedNav,
|
||||
SidebarMixedNav,
|
||||
SidebarNav,
|
||||
@ -35,6 +36,7 @@ const components: Record<LayoutType, Component> = {
|
||||
'sidebar-mixed-nav': SidebarMixedNav,
|
||||
'sidebar-nav': SidebarNav,
|
||||
'header-mixed-nav': HeaderMixedNav,
|
||||
'header-sidebar-nav': HeaderSidebarNav,
|
||||
};
|
||||
|
||||
const PRESET = computed((): PresetItem[] => [
|
||||
@ -53,6 +55,11 @@ const PRESET = computed((): PresetItem[] => [
|
||||
tip: $t('preferences.horizontalTip'),
|
||||
type: 'header-nav',
|
||||
},
|
||||
{
|
||||
name: $t('preferences.headerSidebarNav'),
|
||||
tip: $t('preferences.headerSidebarNavTip'),
|
||||
type: 'header-sidebar-nav',
|
||||
},
|
||||
{
|
||||
name: $t('preferences.mixedMenu'),
|
||||
tip: $t('preferences.mixedMenuTip'),
|
||||
|
@ -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>
|
@ -3,6 +3,7 @@ import HeaderNav from './header-nav.vue';
|
||||
export { default as ContentCompact } from './content-compact.vue';
|
||||
export { default as FullContent } from './full-content.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 SidebarMixedNav } from './sidebar-mixed-nav.vue';
|
||||
export { default as SidebarNav } from './sidebar-nav.vue';
|
||||
|
@ -162,6 +162,7 @@ const {
|
||||
isDark,
|
||||
isFullContent,
|
||||
isHeaderNav,
|
||||
isHeaderSidebarNav,
|
||||
isMixedNav,
|
||||
isSideMixedNav,
|
||||
isSideMode,
|
||||
@ -344,7 +345,8 @@ async function handleReset() {
|
||||
v-model:breadcrumb-show-icon="breadcrumbShowIcon"
|
||||
v-model:breadcrumb-style-type="breadcrumbStyleType"
|
||||
:disabled="
|
||||
!showBreadcrumbConfig || !(isSideNav || isSideMixedNav)
|
||||
!showBreadcrumbConfig ||
|
||||
!(isSideNav || isSideMixedNav || isHeaderSidebarNav)
|
||||
"
|
||||
/>
|
||||
</Block>
|
||||
|
@ -17,6 +17,8 @@
|
||||
"horizontalTip": "Horizontal menu mode, all menus displayed at the top",
|
||||
"twoColumn": "Two Column",
|
||||
"twoColumnTip": "Vertical Two Column Menu Mode",
|
||||
"headerSidebarNav": "Header Vertical",
|
||||
"headerSidebarNavTip": "Header Full Width, Sidebar Navigation Mode",
|
||||
"mixedMenu": "Mixed Menu",
|
||||
"mixedMenuTip": "Vertical & Horizontal Menu Co-exists",
|
||||
"fullContent": "Full Content",
|
||||
|
@ -17,6 +17,8 @@
|
||||
"horizontalTip": "水平菜单模式,菜单全部显示在顶部",
|
||||
"twoColumn": "双列菜单",
|
||||
"twoColumnTip": "垂直双列菜单模式",
|
||||
"headerSidebarNav": "侧边导航",
|
||||
"headerSidebarNavTip": "顶部通栏,侧边导航模式",
|
||||
"headerTwoColumn": "混合双列",
|
||||
"headerTwoColumnTip": "双列、水平菜单共存模式",
|
||||
"mixedMenu": "混合垂直",
|
||||
|
Loading…
Reference in New Issue
Block a user