drawer.vue 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. <script lang="ts" setup>
  2. import type { DrawerProps, ExtendedDrawerApi } from './drawer';
  3. import { ref, watch } from 'vue';
  4. import {
  5. useIsMobile,
  6. usePriorityValue,
  7. useSimpleLocale,
  8. } from '@vben-core/composables';
  9. import { Info, X } from '@vben-core/icons';
  10. import {
  11. Sheet,
  12. SheetClose,
  13. SheetContent,
  14. SheetDescription,
  15. SheetFooter,
  16. SheetHeader,
  17. SheetTitle,
  18. VbenButton,
  19. VbenIconButton,
  20. VbenLoading,
  21. VbenTooltip,
  22. VisuallyHidden,
  23. } from '@vben-core/shadcn-ui';
  24. import { cn } from '@vben-core/shared';
  25. interface Props extends DrawerProps {
  26. class?: string;
  27. contentClass?: string;
  28. drawerApi?: ExtendedDrawerApi;
  29. }
  30. const props = withDefaults(defineProps<Props>(), {
  31. class: '',
  32. contentClass: '',
  33. drawerApi: undefined,
  34. });
  35. const wrapperRef = ref<HTMLElement>();
  36. const { $t } = useSimpleLocale();
  37. const { isMobile } = useIsMobile();
  38. const state = props.drawerApi?.useStore?.();
  39. const title = usePriorityValue('title', props, state);
  40. const description = usePriorityValue('description', props, state);
  41. const titleTooltip = usePriorityValue('titleTooltip', props, state);
  42. const showFooter = usePriorityValue('footer', props, state);
  43. const showLoading = usePriorityValue('loading', props, state);
  44. const closable = usePriorityValue('closable', props, state);
  45. const modal = usePriorityValue('modal', props, state);
  46. const confirmLoading = usePriorityValue('confirmLoading', props, state);
  47. const cancelText = usePriorityValue('cancelText', props, state);
  48. const confirmText = usePriorityValue('confirmText', props, state);
  49. const closeOnClickModal = usePriorityValue('closeOnClickModal', props, state);
  50. const closeOnPressEscape = usePriorityValue('closeOnPressEscape', props, state);
  51. watch(
  52. () => showLoading.value,
  53. (v) => {
  54. if (v && wrapperRef.value) {
  55. wrapperRef.value.scrollTo({
  56. // behavior: 'smooth',
  57. top: 0,
  58. });
  59. }
  60. },
  61. );
  62. function interactOutside(e: Event) {
  63. if (!closeOnClickModal.value) {
  64. e.preventDefault();
  65. }
  66. }
  67. function escapeKeyDown(e: KeyboardEvent) {
  68. if (!closeOnPressEscape.value) {
  69. e.preventDefault();
  70. }
  71. }
  72. // pointer-down-outside
  73. function pointerDownOutside(e: Event) {
  74. const target = e.target as HTMLElement;
  75. const isDismissableModal = !!target?.dataset.dismissableModal;
  76. if (!closeOnClickModal.value || !isDismissableModal) {
  77. e.preventDefault();
  78. }
  79. }
  80. </script>
  81. <template>
  82. <Sheet
  83. :modal="modal"
  84. :open="state?.isOpen"
  85. @update:open="() => drawerApi?.close()"
  86. >
  87. <SheetContent
  88. :class="
  89. cn('flex w-[520px] flex-col', props.class, {
  90. '!w-full': isMobile,
  91. })
  92. "
  93. @escape-key-down="escapeKeyDown"
  94. @interact-outside="interactOutside"
  95. @pointer-down-outside="pointerDownOutside"
  96. >
  97. <SheetHeader
  98. :class="
  99. cn('!flex flex-row items-center justify-between border-b px-6 py-5', {
  100. 'px-4 py-3': closable,
  101. })
  102. "
  103. >
  104. <div>
  105. <SheetTitle v-if="title" class="text-left">
  106. <slot name="title">
  107. {{ title }}
  108. <VbenTooltip v-if="titleTooltip" side="right">
  109. <template #trigger>
  110. <Info class="inline-flex size-5 cursor-pointer pb-1" />
  111. </template>
  112. {{ titleTooltip }}
  113. </VbenTooltip>
  114. </slot>
  115. </SheetTitle>
  116. <SheetDescription v-if="description" class="mt-1 text-xs">
  117. <slot name="description">
  118. {{ description }}
  119. </slot>
  120. </SheetDescription>
  121. </div>
  122. <VisuallyHidden v-if="!title || !description">
  123. <SheetTitle v-if="!title" />
  124. <SheetDescription v-if="!description" />
  125. </VisuallyHidden>
  126. <div class="flex-center">
  127. <slot name="extra"></slot>
  128. <SheetClose
  129. v-if="closable"
  130. as-child
  131. 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"
  132. >
  133. <VbenIconButton>
  134. <X class="size-4" />
  135. </VbenIconButton>
  136. </SheetClose>
  137. </div>
  138. </SheetHeader>
  139. <div
  140. ref="wrapperRef"
  141. :class="
  142. cn('relative flex-1 overflow-y-auto p-3', contentClass, {
  143. 'overflow-hidden': showLoading,
  144. })
  145. "
  146. >
  147. <VbenLoading v-if="showLoading" class="size-full" spinning />
  148. <slot></slot>
  149. </div>
  150. <SheetFooter
  151. v-if="showFooter"
  152. class="w-full flex-row items-center justify-end border-t p-2 px-3"
  153. >
  154. <slot name="prepend-footer"></slot>
  155. <slot name="footer">
  156. <VbenButton variant="ghost" @click="() => drawerApi?.onCancel()">
  157. <slot name="cancelText">
  158. {{ cancelText || $t('cancel') }}
  159. </slot>
  160. </VbenButton>
  161. <VbenButton
  162. :loading="confirmLoading"
  163. @click="() => drawerApi?.onConfirm()"
  164. >
  165. <slot name="confirmText">
  166. {{ confirmText || $t('confirm') }}
  167. </slot>
  168. </VbenButton>
  169. </slot>
  170. <slot name="append-footer"></slot>
  171. </SheetFooter>
  172. </SheetContent>
  173. </Sheet>
  174. </template>