Browse Source

feat: modal&drawer support appendToMain and zIndex (#5092)

* feat: modal/drawer support append to main content

* feat: modal zIndex support

* fix: drawer prop define

* chore: type

* fix: modal/drawer position fixed while append to body

* docs: typo

* chore: add full-width drawer in content area

* chore: remove unnecessary class
Netfan 5 months ago
parent
commit
e419b03cab

+ 8 - 0
docs/src/components/common-ui/vben-drawer.md

@@ -74,6 +74,7 @@ const [Drawer, drawerApi] = useVbenDrawer({
 
 | 属性名 | 描述 | 类型 | 默认值 |
 | --- | --- | --- | --- |
+| appendToMain | 是否挂载到内容区域(默认挂载到body) | `boolean` | `false` |
 | title | 标题 | `string\|slot` | - |
 | titleTooltip | 标题提示信息 | `string\|slot` | - |
 | description | 描述信息 | `string\|slot` | - |
@@ -95,6 +96,13 @@ const [Drawer, drawerApi] = useVbenDrawer({
 | contentClass | modal内容区域的class | `string` | - |
 | footerClass | modal底部区域的class | `string` | - |
 | headerClass | modal顶部区域的class | `string` | - |
+| zIndex | 抽屉的ZIndex层级 | `number` | `1000` |
+
+::: info appendToMain
+
+`appendToMain`可以指定将抽屉挂载到内容区域,打开抽屉时,内容区域以外的部分(标签栏、导航菜单等等)不会被遮挡。默认情况下,抽屉会挂载到body上。但是:挂载到内容区域时,作为页面根容器的`Page`组件,需要设置`auto-content-height`属性,以便抽屉能够正确计算高度。
+
+:::
 
 ### Event
 

+ 8 - 0
docs/src/components/common-ui/vben-modal.md

@@ -80,6 +80,7 @@ const [Modal, modalApi] = useVbenModal({
 
 | 属性名 | 描述 | 类型 | 默认值 |
 | --- | --- | --- | --- |
+| appendToMain | 是否挂载到内容区域(默认挂载到body) | `boolean` | `false` |
 | title | 标题 | `string\|slot` | - |
 | titleTooltip | 标题提示信息 | `string\|slot` | - |
 | description | 描述信息 | `string\|slot` | - |
@@ -106,6 +107,13 @@ const [Modal, modalApi] = useVbenModal({
 | footerClass | modal底部区域的class | `string` | - |
 | headerClass | modal顶部区域的class | `string` | - |
 | bordered | 是否显示border | `boolean` | `false` |
+| zIndex | 弹窗的ZIndex层级 | `number` | `1000` |
+
+::: info appendToMain
+
+`appendToMain`可以指定将弹窗挂载到内容区域,打开带这招的弹窗时,内容区域以外的部分(标签栏、导航菜单等等)不会被遮挡。默认情况下,弹窗会挂载到body上。但是:挂载到内容区域时,作为页面根容器的`Page`组件,需要设置`auto-content-height`属性,以便弹窗能够正确计算高度。
+
+:::
 
 ### Event
 

+ 3 - 0
packages/@core/base/shared/src/constants/globals.ts

@@ -7,6 +7,9 @@ export const CSS_VARIABLE_LAYOUT_HEADER_HEIGHT = `--vben-header-height`;
 /** layout footer 组件的高度 */
 export const CSS_VARIABLE_LAYOUT_FOOTER_HEIGHT = `--vben-footer-height`;
 
+/** 内容区域的组件ID */
+export const ELEMENT_ID_MAIN_CONTENT = `__vben_main_content`;
+
 /**
  * @zh_CN 默认命名空间
  */

+ 1 - 0
packages/@core/ui-kit/layout-ui/package.json

@@ -40,6 +40,7 @@
     "@vben-core/composables": "workspace:*",
     "@vben-core/icons": "workspace:*",
     "@vben-core/shadcn-ui": "workspace:*",
+    "@vben-core/shared": "workspace:*",
     "@vben-core/typings": "workspace:*",
     "@vueuse/core": "catalog:",
     "vue": "catalog:"

+ 4 - 0
packages/@core/ui-kit/layout-ui/src/vben-layout.vue

@@ -11,6 +11,7 @@ import {
 } from '@vben-core/composables';
 import { Menu } from '@vben-core/icons';
 import { VbenIconButton } from '@vben-core/shadcn-ui';
+import { ELEMENT_ID_MAIN_CONTENT } from '@vben-core/shared/constants';
 
 import { useMouse, useScroll, useThrottleFn } from '@vueuse/core';
 
@@ -457,6 +458,8 @@ function handleHeaderToggle() {
     emit('toggleSidebar');
   }
 }
+
+const idMainContent = ELEMENT_ID_MAIN_CONTENT;
 </script>
 
 <template>
@@ -553,6 +556,7 @@ function handleHeaderToggle() {
 
       <!-- </div> -->
       <LayoutContent
+        :id="idMainContent"
         :content-compact="contentCompact"
         :content-compact-width="contentCompactWidth"
         :padding="contentPadding"

+ 11 - 2
packages/@core/ui-kit/popup-ui/src/drawer/drawer.ts

@@ -7,6 +7,11 @@ import type { Component, Ref } from 'vue';
 export type DrawerPlacement = 'bottom' | 'left' | 'right' | 'top';
 
 export interface DrawerProps {
+  /**
+   * 是否挂载到内容区域
+   * @default false
+   */
+  appendToMain?: boolean;
   /**
    * 取消按钮文字
    */
@@ -59,12 +64,12 @@ export interface DrawerProps {
    * 弹窗头部样式
    */
   headerClass?: ClassType;
-
   /**
    * 弹窗是否显示
    * @default false
    */
   loading?: boolean;
+
   /**
    * 是否显示遮罩
    * @default true
@@ -74,12 +79,12 @@ export interface DrawerProps {
    * 是否自动聚焦
    */
   openAutoFocus?: boolean;
-
   /**
    * 抽屉位置
    * @default right
    */
   placement?: DrawerPlacement;
+
   /**
    * 是否显示取消按钮
    * @default true
@@ -98,6 +103,10 @@ export interface DrawerProps {
    * 弹窗标题提示
    */
   titleTooltip?: string;
+  /**
+   * 抽屉层级
+   */
+  zIndex?: number;
 }
 
 export interface DrawerState extends DrawerProps {

+ 12 - 1
packages/@core/ui-kit/popup-ui/src/drawer/drawer.vue

@@ -1,7 +1,7 @@
 <script lang="ts" setup>
 import type { DrawerProps, ExtendedDrawerApi } from './drawer';
 
-import { provide, ref, useId, watch } from 'vue';
+import { computed, provide, ref, useId, watch } from 'vue';
 
 import {
   useIsMobile,
@@ -23,6 +23,7 @@ import {
   VbenLoading,
   VisuallyHidden,
 } from '@vben-core/shadcn-ui';
+import { ELEMENT_ID_MAIN_CONTENT } from '@vben-core/shared/constants';
 import { globalShareState } from '@vben-core/shared/global-state';
 import { cn } from '@vben-core/shared/utils';
 
@@ -31,7 +32,9 @@ interface Props extends DrawerProps {
 }
 
 const props = withDefaults(defineProps<Props>(), {
+  appendToMain: false,
   drawerApi: undefined,
+  zIndex: 1000,
 });
 
 const components = globalShareState.getComponents();
@@ -46,6 +49,7 @@ const { isMobile } = useIsMobile();
 const state = props.drawerApi?.useStore?.();
 
 const {
+  appendToMain,
   cancelText,
   class: drawerClass,
   closable,
@@ -67,6 +71,7 @@ const {
   showConfirmButton,
   title,
   titleTooltip,
+  zIndex,
 } = usePriorityValues(props, state);
 
 watch(
@@ -110,6 +115,10 @@ function handleFocusOutside(e: Event) {
   e.preventDefault();
   e.stopPropagation();
 }
+
+const getAppendTo = computed(() => {
+  return appendToMain.value ? `#${ELEMENT_ID_MAIN_CONTENT}` : undefined;
+});
 </script>
 <template>
   <Sheet
@@ -118,6 +127,7 @@ function handleFocusOutside(e: Event) {
     @update:open="() => drawerApi?.close()"
   >
     <SheetContent
+      :append-to="getAppendTo"
       :class="
         cn('flex w-[520px] flex-col', drawerClass, {
           '!w-full': isMobile || placement === 'bottom' || placement === 'top',
@@ -127,6 +137,7 @@ function handleFocusOutside(e: Event) {
       :modal="modal"
       :open="state?.isOpen"
       :side="placement"
+      :z-index="zIndex"
       @close-auto-focus="handleFocusOutside"
       @escape-key-down="escapeKeyDown"
       @focus-outside="handleFocusOutside"

+ 10 - 1
packages/@core/ui-kit/popup-ui/src/modal/modal.ts

@@ -3,6 +3,11 @@ import type { ModalApi } from './modal-api';
 import type { Component, Ref } from 'vue';
 
 export interface ModalProps {
+  /**
+   * 是否要挂载到内容区域
+   * @default false
+   */
+  appendToMain?: boolean;
   /**
    * 是否显示边框
    * @default false
@@ -12,7 +17,6 @@ export interface ModalProps {
    * 取消按钮文字
    */
   cancelText?: string;
-
   /**
    * 是否居中
    * @default false
@@ -20,6 +24,7 @@ export interface ModalProps {
   centered?: boolean;
 
   class?: string;
+
   /**
    * 是否显示右上角的关闭按钮
    * @default true
@@ -112,6 +117,10 @@ export interface ModalProps {
    * 弹窗标题提示
    */
   titleTooltip?: string;
+  /**
+   * 弹窗层级
+   */
+  zIndex?: number;
 }
 
 export interface ModalState extends ModalProps {

+ 9 - 0
packages/@core/ui-kit/popup-ui/src/modal/modal.vue

@@ -22,6 +22,7 @@ import {
   VbenLoading,
   VisuallyHidden,
 } from '@vben-core/shadcn-ui';
+import { ELEMENT_ID_MAIN_CONTENT } from '@vben-core/shared/constants';
 import { globalShareState } from '@vben-core/shared/global-state';
 import { cn } from '@vben-core/shared/utils';
 
@@ -32,6 +33,7 @@ interface Props extends ModalProps {
 }
 
 const props = withDefaults(defineProps<Props>(), {
+  appendToMain: false,
   modalApi: undefined,
 });
 
@@ -52,6 +54,7 @@ const { isMobile } = useIsMobile();
 const state = props.modalApi?.useStore?.();
 
 const {
+  appendToMain,
   bordered,
   cancelText,
   centered,
@@ -78,6 +81,7 @@ const {
   showConfirmButton,
   title,
   titleTooltip,
+  zIndex,
 } = usePriorityValues(props, state);
 
 const shouldFullscreen = computed(
@@ -161,6 +165,9 @@ function handleFocusOutside(e: Event) {
   e.preventDefault();
   e.stopPropagation();
 }
+const getAppendTo = computed(() => {
+  return appendToMain.value ? `#${ELEMENT_ID_MAIN_CONTENT}` : undefined;
+});
 </script>
 <template>
   <Dialog
@@ -170,6 +177,7 @@ function handleFocusOutside(e: Event) {
   >
     <DialogContent
       ref="contentRef"
+      :append-to="getAppendTo"
       :class="
         cn(
           'left-0 right-0 top-[10vh] mx-auto flex max-h-[80%] w-[520px] flex-col p-0 sm:rounded-[var(--radius)]',
@@ -187,6 +195,7 @@ function handleFocusOutside(e: Event) {
       :modal="modal"
       :open="state?.isOpen"
       :show-close="closable"
+      :z-index="zIndex"
       close-class="top-3"
       @close-auto-focus="handleFocusOutside"
       @closed="() => modalApi?.onClosed()"

+ 23 - 4
packages/@core/ui-kit/shadcn-ui/src/ui/dialog/DialogContent.vue

@@ -20,14 +20,16 @@ import DialogOverlay from './DialogOverlay.vue';
 const props = withDefaults(
   defineProps<
     {
+      appendTo?: HTMLElement | string;
       class?: ClassType;
       closeClass?: ClassType;
       modal?: boolean;
       open?: boolean;
       showClose?: boolean;
+      zIndex?: number;
     } & DialogContentProps
   >(),
-  { showClose: true },
+  { appendTo: 'body', showClose: true, zIndex: 1000 },
 );
 const emits = defineEmits<
   { close: []; closed: []; opened: [] } & DialogContentEmits
@@ -45,6 +47,18 @@ const delegatedProps = computed(() => {
   return delegated;
 });
 
+function isAppendToBody() {
+  return (
+    props.appendTo === 'body' ||
+    props.appendTo === document.body ||
+    !props.appendTo
+  );
+}
+
+const position = computed(() => {
+  return isAppendToBody() ? 'fixed' : 'absolute';
+});
+
 const forwarded = useForwardPropsEmits(delegatedProps, emits);
 
 const contentRef = ref<InstanceType<typeof DialogContent> | null>(null);
@@ -64,17 +78,22 @@ defineExpose({
 </script>
 
 <template>
-  <DialogPortal>
+  <DialogPortal :to="appendTo">
     <Transition name="fade">
-      <DialogOverlay v-if="open && modal" @click="() => emits('close')" />
+      <DialogOverlay
+        v-if="open && modal"
+        :style="{ zIndex, position }"
+        @click="() => emits('close')"
+      />
     </Transition>
     <DialogContent
       ref="contentRef"
+      :style="{ zIndex, position }"
       @animationend="onAnimationEnd"
       v-bind="forwarded"
       :class="
         cn(
-          'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-top-[48%] fixed z-[1000] w-full p-6 shadow-lg outline-none sm:rounded-xl',
+          'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-top-[48%] w-full p-6 shadow-lg outline-none sm:rounded-xl',
           props.class,
         )
       "

+ 1 - 4
packages/@core/ui-kit/shadcn-ui/src/ui/dialog/DialogOverlay.vue

@@ -7,8 +7,5 @@ useScrollLock();
 const id = inject('DISMISSABLE_MODAL_ID');
 </script>
 <template>
-  <div
-    :data-dismissable-modal="id"
-    class="bg-overlay fixed inset-0 z-[1000]"
-  ></div>
+  <div :data-dismissable-modal="id" class="bg-overlay inset-0"></div>
 </template>

+ 7 - 2
packages/@core/ui-kit/shadcn-ui/src/ui/dialog/DialogScrollContent.vue

@@ -14,7 +14,10 @@ import {
   useForwardPropsEmits,
 } from 'radix-vue';
 
-const props = defineProps<{ class?: any } & DialogContentProps>();
+const props = withDefaults(
+  defineProps<{ class?: any; zIndex?: number } & DialogContentProps>(),
+  { zIndex: 1000 },
+);
 const emits = defineEmits<DialogContentEmits>();
 
 const delegatedProps = computed(() => {
@@ -29,7 +32,8 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
 <template>
   <DialogPortal>
     <DialogOverlay
-      class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 border-border fixed inset-0 z-[1000] grid place-items-center overflow-y-auto border bg-black/80"
+      :style="{ zIndex }"
+      class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 border-border absolute inset-0 grid place-items-center overflow-y-auto border bg-black/80"
     >
       <DialogContent
         :class="
@@ -38,6 +42,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
             props.class,
           )
         "
+        :style="{ zIndex }"
         v-bind="forwarded"
         @pointer-down-outside="
           (event) => {

+ 22 - 4
packages/@core/ui-kit/shadcn-ui/src/ui/sheet/SheetContent.vue

@@ -15,17 +15,22 @@ import { type SheetVariants, sheetVariants } from './sheet';
 import SheetOverlay from './SheetOverlay.vue';
 
 interface SheetContentProps extends DialogContentProps {
+  appendTo?: HTMLElement | string;
   class?: any;
   modal?: boolean;
   open?: boolean;
   side?: SheetVariants['side'];
+  zIndex?: number;
 }
 
 defineOptions({
   inheritAttrs: false,
 });
 
-const props = defineProps<SheetContentProps>();
+const props = withDefaults(defineProps<SheetContentProps>(), {
+  appendTo: 'body',
+  zIndex: 1000,
+});
 
 const emits = defineEmits<DialogContentEmits>();
 
@@ -41,16 +46,29 @@ const delegatedProps = computed(() => {
   return delegated;
 });
 
+function isAppendToBody() {
+  return (
+    props.appendTo === 'body' ||
+    props.appendTo === document.body ||
+    !props.appendTo
+  );
+}
+
+const position = computed(() => {
+  return isAppendToBody() ? 'fixed' : 'absolute';
+});
+
 const forwarded = useForwardPropsEmits(delegatedProps, emits);
 </script>
 
 <template>
-  <DialogPortal>
+  <DialogPortal :to="appendTo">
     <Transition name="fade">
-      <SheetOverlay v-if="open && modal" />
+      <SheetOverlay v-if="open && modal" :style="{ zIndex, position }" />
     </Transition>
     <DialogContent
-      :class="cn(sheetVariants({ side }), 'z-[1000]', props.class)"
+      :class="cn(sheetVariants({ side }), props.class)"
+      :style="{ zIndex, position }"
       v-bind="{ ...forwarded, ...$attrs }"
     >
       <slot></slot>

+ 1 - 4
packages/@core/ui-kit/shadcn-ui/src/ui/sheet/SheetOverlay.vue

@@ -7,8 +7,5 @@ useScrollLock();
 const id = inject('DISMISSABLE_DRAWER_ID');
 </script>
 <template>
-  <div
-    :data-dismissable-drawer="id"
-    class="bg-overlay fixed inset-0 z-[1000]"
-  ></div>
+  <div :data-dismissable-drawer="id" class="bg-overlay inset-0"></div>
 </template>

+ 2 - 2
packages/@core/ui-kit/shadcn-ui/src/ui/sheet/sheet.ts

@@ -1,7 +1,7 @@
 import { cva, type VariantProps } from 'class-variance-authority';
 
 export const sheetVariants = cva(
-  'fixed z-[1000] bg-background shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500 border-border',
+  'bg-background shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500 border-border',
   {
     defaultVariants: {
       side: 'right',
@@ -12,7 +12,7 @@ export const sheetVariants = cva(
           'inset-x-0 bottom-0 border-t border-border data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
         left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left ',
         right:
-          'inset-y-0 right-0 h-full w-3/4 border-l  data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right',
+          'inset-y-0 right-0 w-3/4 border-l  data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right',
         top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
       },
     },

+ 10 - 3
packages/effects/common-ui/src/components/page/page.vue

@@ -1,5 +1,12 @@
 <script setup lang="ts">
-import { computed, nextTick, onMounted, ref, useTemplateRef } from 'vue';
+import {
+  computed,
+  nextTick,
+  onMounted,
+  ref,
+  type StyleValue,
+  useTemplateRef,
+} from 'vue';
 
 import { CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT } from '@vben-core/shared/constants';
 import { cn } from '@vben-core/shared/utils';
@@ -29,13 +36,13 @@ const shouldAutoHeight = ref(false);
 const headerRef = useTemplateRef<HTMLDivElement>('headerRef');
 const footerRef = useTemplateRef<HTMLDivElement>('footerRef');
 
-const contentStyle = computed(() => {
+const contentStyle = computed<StyleValue>(() => {
   if (autoContentHeight) {
     return {
       height: shouldAutoHeight.value
         ? `calc(var(${CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT}) - ${headerHeight.value}px)`
         : '0',
-      // 'overflow-y': shouldAutoHeight.value?'auto':'unset',
+      overflowY: shouldAutoHeight.value ? 'auto' : 'unset',
     };
   }
   return {};

+ 21 - 0
playground/src/views/examples/drawer/in-content-demo.vue

@@ -0,0 +1,21 @@
+<script lang="ts" setup>
+import { useVbenDrawer } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+const [Drawer, drawerApi] = useVbenDrawer({
+  onCancel() {
+    drawerApi.close();
+  },
+  onConfirm() {
+    message.info('onConfirm');
+    // drawerApi.close();
+  },
+});
+</script>
+<template>
+  <Drawer append-to-main title="基础抽屉示例" title-tooltip="标题提示内容">
+    <template #extra> extra </template>
+    本抽屉指定在内容区域打开
+  </Drawer>
+</template>

+ 67 - 4
playground/src/views/examples/drawer/index.vue

@@ -8,6 +8,7 @@ import AutoHeightDemo from './auto-height-demo.vue';
 import BaseDemo from './base-demo.vue';
 import DynamicDemo from './dynamic-demo.vue';
 import FormDrawerDemo from './form-drawer-demo.vue';
+import inContentDemo from './in-content-demo.vue';
 import SharedDataDemo from './shared-data-demo.vue';
 
 const [BaseDrawer, baseDrawerApi] = useVbenDrawer({
@@ -16,6 +17,12 @@ const [BaseDrawer, baseDrawerApi] = useVbenDrawer({
   // placement: 'left',
 });
 
+const [InContentDrawer, inContentDrawerApi] = useVbenDrawer({
+  // 连接抽离的组件
+  connectedComponent: inContentDemo,
+  // placement: 'left',
+});
+
 const [AutoHeightDrawer, autoHeightDrawerApi] = useVbenDrawer({
   connectedComponent: AutoHeightDemo,
 });
@@ -37,6 +44,23 @@ function openBaseDrawer(placement: DrawerPlacement = 'right') {
   baseDrawerApi.open();
 }
 
+function openInContentDrawer(placement: DrawerPlacement = 'right') {
+  inContentDrawerApi.setState({ class: '', placement });
+  if (placement === 'top') {
+    // 页面顶部区域的层级只有200,所以设置一个低于200的值,抽屉从顶部滑出来的时候才比较合适
+    inContentDrawerApi.setState({ zIndex: 199 });
+  } else {
+    inContentDrawerApi.setState({ zIndex: undefined });
+  }
+  inContentDrawerApi.open();
+}
+
+function openMaxContentDrawer() {
+  // 这里只是用来演示方便。实际上自己使用的时候可以直接将这些配置卸载Drawer的属性里
+  inContentDrawerApi.setState({ class: 'w-full', placement: 'right' });
+  inContentDrawerApi.open();
+}
+
 function openAutoHeightDrawer() {
   autoHeightDrawerApi.open();
 }
@@ -69,6 +93,7 @@ function openFormDrawer() {
 
 <template>
   <Page
+    auto-content-height
     description="抽屉组件通常用于在当前页面上显示一个覆盖层,用以展示重要信息或提供用户交互界面。"
     title="抽屉组件示例"
   >
@@ -76,6 +101,7 @@ function openFormDrawer() {
       <DocButton path="/components/common-ui/vben-drawer" />
     </template>
     <BaseDrawer />
+    <InContentDrawer />
     <AutoHeightDrawer />
     <DynamicDrawer />
     <SharedDataDrawer />
@@ -83,18 +109,55 @@ function openFormDrawer() {
 
     <Card class="mb-4" title="基本使用">
       <p class="mb-3">一个基础的抽屉示例</p>
-      <Button type="primary" @click="openBaseDrawer('right')">右侧打开</Button>
-      <Button class="ml-2" type="primary" @click="openBaseDrawer('bottom')">
+      <Button class="mb-2" type="primary" @click="openBaseDrawer('right')">
+        右侧打开
+      </Button>
+      <Button
+        class="mb-2 ml-2"
+        type="primary"
+        @click="openBaseDrawer('bottom')"
+      >
         底部打开
       </Button>
-      <Button class="ml-2" type="primary" @click="openBaseDrawer('left')">
+      <Button class="mb-2 ml-2" type="primary" @click="openBaseDrawer('left')">
         左侧打开
       </Button>
-      <Button class="ml-2" type="primary" @click="openBaseDrawer('top')">
+      <Button class="mb-2 ml-2" type="primary" @click="openBaseDrawer('top')">
         顶部打开
       </Button>
     </Card>
 
+    <Card class="mb-4" title="在内容区域打开">
+      <p class="mb-3">指定抽屉在内容区域打开,不会覆盖顶部和左侧菜单等区域</p>
+      <Button class="mb-2" type="primary" @click="openInContentDrawer('right')">
+        右侧打开
+      </Button>
+      <Button
+        class="mb-2 ml-2"
+        type="primary"
+        @click="openInContentDrawer('bottom')"
+      >
+        底部打开
+      </Button>
+      <Button
+        class="mb-2 ml-2"
+        type="primary"
+        @click="openInContentDrawer('left')"
+      >
+        左侧打开
+      </Button>
+      <Button
+        class="mb-2 ml-2"
+        type="primary"
+        @click="openInContentDrawer('top')"
+      >
+        顶部打开
+      </Button>
+      <Button class="mb-2 ml-2" type="primary" @click="openMaxContentDrawer">
+        内容区域全屏打开
+      </Button>
+    </Card>
+
     <Card class="mb-4" title="内容高度自适应滚动">
       <p class="mb-3">可根据内容自动计算滚动高度</p>
       <Button type="primary" @click="openAutoHeightDrawer">打开抽屉</Button>

+ 25 - 0
playground/src/views/examples/modal/in-content-demo.vue

@@ -0,0 +1,25 @@
+<script lang="ts" setup>
+import { useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+const [Modal, modalApi] = useVbenModal({
+  onCancel() {
+    modalApi.close();
+  },
+  onConfirm() {
+    message.info('onConfirm');
+    // modalApi.close();
+  },
+});
+</script>
+<template>
+  <Modal
+    append-to-main
+    class="w-[600px]"
+    title="基础弹窗示例"
+    title-tooltip="标题提示内容"
+  >
+    此弹窗指定在内容区域打开
+  </Modal>
+</template>

+ 17 - 0
playground/src/views/examples/modal/index.vue

@@ -9,6 +9,7 @@ import BaseDemo from './base-demo.vue';
 import DragDemo from './drag-demo.vue';
 import DynamicDemo from './dynamic-demo.vue';
 import FormModalDemo from './form-modal-demo.vue';
+import InContentModalDemo from './in-content-demo.vue';
 import SharedDataDemo from './shared-data-demo.vue';
 
 const [BaseModal, baseModalApi] = useVbenModal({
@@ -16,6 +17,11 @@ const [BaseModal, baseModalApi] = useVbenModal({
   connectedComponent: BaseDemo,
 });
 
+const [InContentModal, inContentModalApi] = useVbenModal({
+  // 连接抽离的组件
+  connectedComponent: InContentModalDemo,
+});
+
 const [AutoHeightModal, autoHeightModalApi] = useVbenModal({
   connectedComponent: AutoHeightDemo,
 });
@@ -40,6 +46,10 @@ function openBaseModal() {
   baseModalApi.open();
 }
 
+function openInContentModal() {
+  inContentModalApi.open();
+}
+
 function openAutoHeightModal() {
   autoHeightModalApi.open();
 }
@@ -76,6 +86,7 @@ function openFormModal() {
 
 <template>
   <Page
+    auto-content-height
     description="弹窗组件常用于在不离开当前页面的情况下,显示额外的信息、表单或操作提示,更多api请查看组件文档。"
     title="弹窗组件示例"
   >
@@ -83,6 +94,7 @@ function openFormModal() {
       <DocButton path="/components/common-ui/vben-modal" />
     </template>
     <BaseModal />
+    <InContentModal />
     <AutoHeightModal />
     <DragModal />
     <DynamicModal />
@@ -93,6 +105,11 @@ function openFormModal() {
       <Button type="primary" @click="openBaseModal">打开弹窗</Button>
     </Card>
 
+    <Card class="mb-4" title="指定容器">
+      <p class="mb-3">在内容区域打开弹窗的示例</p>
+      <Button type="primary" @click="openInContentModal">打开弹窗</Button>
+    </Card>
+
     <Card class="mb-4" title="内容高度自适应">
       <p class="mb-3">可根据内容并自动调整高度</p>
       <Button type="primary" @click="openAutoHeightModal">打开弹窗</Button>

+ 6 - 0
playground/src/views/examples/vxe-table/basic.vue

@@ -76,6 +76,12 @@ function changeLoading() {
     <template #extra>
       <DocButton path="/components/common-ui/vben-vxe-table" />
     </template>
+    <Modal title="弹窗测试">
+      <p>这是一个弹窗</p>
+    </Modal>
+    <Drawer title="抽屉测试">
+      <p>这是一个抽屉</p>
+    </Drawer>
     <Grid table-title="基础列表" table-title-help="提示">
       <!-- <template #toolbar-actions>
         <Button class="mr-2" type="primary">左侧插槽</Button>

+ 3 - 0
pnpm-lock.yaml

@@ -1340,6 +1340,9 @@ importers:
       '@vben-core/shadcn-ui':
         specifier: workspace:*
         version: link:../shadcn-ui
+      '@vben-core/shared':
+        specifier: workspace:*
+        version: link:../../base/shared
       '@vben-core/typings':
         specifier: workspace:*
         version: link:../../base/typings